import 'package:flutter/foundation.dart'; /// Role of a message inside a Copilot conversation. enum CopilotRole { user, assistant } /// A single reference image returned alongside an assistant answer /// (e.g. a scanned page from a referenced document). class CopilotImage { final String base64; final String source; final int page; final String? fileLink; final double? similarityScore; CopilotImage({ required this.base64, required this.source, required this.page, this.fileLink, this.similarityScore, }); factory CopilotImage.fromJson(Map json) => CopilotImage( base64: json['image_base64'] ?? '', source: json['source'] ?? '', page: json['page'] is int ? json['page'] : int.tryParse('${json['page']}') ?? 0, fileLink: json['file_link'], similarityScore: (json['similarity_score'] as num?)?.toDouble(), ); } /// A single message (human query or assistant answer) inside a Copilot conversation. /// /// This is intentionally a NEW, small model (not a reuse of chat's SingleUserChatModel), /// since the Copilot Automation API response shape (answer/document_answer/website_answer/images) /// is completely different from the multi-user chat history model. class CopilotMessage { final String id; final CopilotRole role; String content; // assistant answer text OR the human query text String? documentAnswer; String? websiteAnswer; List images; DateTime? timestamp; bool isStreaming; bool isError; // Local path of a recorded audio file for a user's voice query (never a remote // download URL - the backend never sends audio back). Used to let the user // replay their own sent voice message, same as Chat's audio playback. String? localAudioPath; CopilotMessage({ required this.id, required this.role, this.content = '', this.documentAnswer, this.websiteAnswer, this.images = const [], this.timestamp, this.isStreaming = false, this.isError = false, this.localAudioPath, }); /// Builds a message from GetConversationByThreadId's flat message shape: /// { "role": "human"|"ai", "content": "...", "document_answer": "...", "website_answer": "...", "images": [...] } /// NOTE: the API currently always returns `timestamp: null`, so we fall back to /// "now" purely so the UI has a value to render (matches Chat's bubble layout, /// which always shows a time under each message). factory CopilotMessage.fromHistoryJson(Map json) => CopilotMessage( id: UniqueKey().toString(), role: json['role'] == 'ai' ? CopilotRole.assistant : CopilotRole.user, content: json['content'] ?? '', documentAnswer: json['document_answer'], websiteAnswer: json['website_answer'], images: (json['images'] as List? ?? []).map((e) => CopilotImage.fromJson(e)).toList(), timestamp: json['timestamp'] != null ? (DateTime.tryParse(json['timestamp'].toString()) ?? DateTime.now()) : DateTime.now(), ); } /// A single event decoded from a QueryStream SSE `data:` line /// (payload shape: {"type":"response","content":{...}}, or the literal string "[DONE]"). class CopilotStreamEvent { final String type; final CopilotStreamContent? content; CopilotStreamEvent({required this.type, this.content}); factory CopilotStreamEvent.fromJson(Map json) => CopilotStreamEvent( type: json['type'] ?? '', content: json['content'] is Map ? CopilotStreamContent.fromJson(json['content']) : null, ); } class CopilotStreamContent { final String? answer; final String? documentAnswer; final String? websiteAnswer; final int? chunksUsed; final List images; CopilotStreamContent({ this.answer, this.documentAnswer, this.websiteAnswer, this.chunksUsed, this.images = const [], }); factory CopilotStreamContent.fromJson(Map json) => CopilotStreamContent( answer: json['answer'], documentAnswer: json['document_answer'], websiteAnswer: json['website_answer'], chunksUsed: json['chunks_used'], images: (json['images'] as List? ?? []).map((e) => CopilotImage.fromJson(e)).toList(), ); } /// A single row in the Copilot query history list (GetQueryHistory). class CopilotHistoryItem { final String threadId; final String firstHumanMessage; final DateTime updatedAt; CopilotHistoryItem({ required this.threadId, required this.firstHumanMessage, required this.updatedAt, }); factory CopilotHistoryItem.fromJson(Map json) => CopilotHistoryItem( threadId: json['threadId'] ?? '', firstHumanMessage: json['firstHumanMessage'] ?? '', updatedAt: DateTime.tryParse(json['updatedAt'] ?? '') ?? DateTime.now(), ); }