@@ -6,8 +6,10 @@ use std::sync::Arc;
66use tauri:: { AppHandle , State } ;
77
88use crate :: api:: app_state:: AppState ;
9+ use crate :: api:: context_upload_api:: get_image_context;
910use bitfun_core:: agentic:: coordination:: ConversationCoordinator ;
1011use bitfun_core:: agentic:: core:: * ;
12+ use bitfun_core:: agentic:: image_analysis:: ImageContextData ;
1113
1214#[ derive( Debug , Deserialize ) ]
1315#[ serde( rename_all = "camelCase" ) ]
@@ -45,6 +47,8 @@ pub struct StartDialogTurnRequest {
4547 pub user_input : String ,
4648 pub agent_type : String ,
4749 pub turn_id : Option < String > ,
50+ #[ serde( default ) ]
51+ pub image_contexts : Option < Vec < ImageContextData > > ,
4852}
4953
5054#[ derive( Debug , Serialize ) ]
@@ -179,23 +183,131 @@ pub async fn start_dialog_turn(
179183 coordinator : State < ' _ , Arc < ConversationCoordinator > > ,
180184 request : StartDialogTurnRequest ,
181185) -> Result < StartDialogTurnResponse , String > {
182- let _stream = coordinator
183- . start_dialog_turn (
184- request. session_id ,
185- request. user_input ,
186- request. turn_id ,
187- request. agent_type ,
188- false ,
189- )
190- . await
191- . map_err ( |e| format ! ( "Failed to start dialog turn: {}" , e) ) ?;
186+ let StartDialogTurnRequest {
187+ session_id,
188+ user_input,
189+ agent_type,
190+ turn_id,
191+ image_contexts,
192+ } = request;
193+
194+ if let Some ( image_contexts) = image_contexts
195+ . as_ref ( )
196+ . filter ( |images| !images. is_empty ( ) )
197+ . cloned ( )
198+ {
199+ let resolved_image_contexts = resolve_missing_image_payloads ( image_contexts) ?;
200+ coordinator
201+ . start_dialog_turn_with_image_contexts (
202+ session_id,
203+ user_input,
204+ resolved_image_contexts,
205+ turn_id,
206+ agent_type,
207+ )
208+ . await
209+ . map_err ( |e| format ! ( "Failed to start dialog turn: {}" , e) ) ?;
210+ } else {
211+ coordinator
212+ . start_dialog_turn (
213+ session_id,
214+ user_input,
215+ turn_id,
216+ agent_type,
217+ false ,
218+ )
219+ . await
220+ . map_err ( |e| format ! ( "Failed to start dialog turn: {}" , e) ) ?;
221+ }
192222
193223 Ok ( StartDialogTurnResponse {
194224 success : true ,
195225 message : "Dialog turn started" . to_string ( ) ,
196226 } )
197227}
198228
229+ fn is_blank_text ( value : Option < & String > ) -> bool {
230+ value. map ( |s| s. trim ( ) . is_empty ( ) ) . unwrap_or ( true )
231+ }
232+
233+ fn resolve_missing_image_payloads (
234+ image_contexts : Vec < ImageContextData > ,
235+ ) -> Result < Vec < ImageContextData > , String > {
236+ let mut resolved = Vec :: with_capacity ( image_contexts. len ( ) ) ;
237+
238+ for mut image in image_contexts {
239+ let missing_payload =
240+ is_blank_text ( image. image_path . as_ref ( ) ) && is_blank_text ( image. data_url . as_ref ( ) ) ;
241+ if !missing_payload {
242+ resolved. push ( image) ;
243+ continue ;
244+ }
245+
246+ let stored = get_image_context ( & image. id ) . ok_or_else ( || {
247+ format ! (
248+ "Image context not found for image_id={}. It may have expired. Please re-attach the image and retry." ,
249+ image. id
250+ )
251+ } ) ?;
252+
253+ if is_blank_text ( image. image_path . as_ref ( ) ) {
254+ image. image_path = stored
255+ . image_path
256+ . clone ( )
257+ . filter ( |s| !s. trim ( ) . is_empty ( ) ) ;
258+ }
259+ if is_blank_text ( image. data_url . as_ref ( ) ) {
260+ image. data_url = stored
261+ . data_url
262+ . clone ( )
263+ . filter ( |s| !s. trim ( ) . is_empty ( ) ) ;
264+ }
265+ if image. mime_type . trim ( ) . is_empty ( ) {
266+ image. mime_type = stored. mime_type . clone ( ) ;
267+ }
268+
269+ let mut metadata = image. metadata . take ( ) . unwrap_or_else ( || serde_json:: json!( { } ) ) ;
270+ if !metadata. is_object ( ) {
271+ metadata = serde_json:: json!( { "raw_metadata" : metadata } ) ;
272+ }
273+ if let Some ( obj) = metadata. as_object_mut ( ) {
274+ if !obj. contains_key ( "name" ) {
275+ obj. insert ( "name" . to_string ( ) , serde_json:: json!( stored. image_name) ) ;
276+ }
277+ if !obj. contains_key ( "width" ) {
278+ obj. insert ( "width" . to_string ( ) , serde_json:: json!( stored. width) ) ;
279+ }
280+ if !obj. contains_key ( "height" ) {
281+ obj. insert ( "height" . to_string ( ) , serde_json:: json!( stored. height) ) ;
282+ }
283+ if !obj. contains_key ( "file_size" ) {
284+ obj. insert ( "file_size" . to_string ( ) , serde_json:: json!( stored. file_size) ) ;
285+ }
286+ if !obj. contains_key ( "source" ) {
287+ obj. insert ( "source" . to_string ( ) , serde_json:: json!( stored. source) ) ;
288+ }
289+ obj. insert (
290+ "resolved_from_upload_cache" . to_string ( ) ,
291+ serde_json:: json!( true ) ,
292+ ) ;
293+ }
294+ image. metadata = Some ( metadata) ;
295+
296+ let still_missing =
297+ is_blank_text ( image. image_path . as_ref ( ) ) && is_blank_text ( image. data_url . as_ref ( ) ) ;
298+ if still_missing {
299+ return Err ( format ! (
300+ "Image context {} is missing image_path/data_url after cache resolution" ,
301+ image. id
302+ ) ) ;
303+ }
304+
305+ resolved. push ( image) ;
306+ }
307+
308+ Ok ( resolved)
309+ }
310+
199311#[ tauri:: command]
200312pub async fn cancel_dialog_turn (
201313 coordinator : State < ' _ , Arc < ConversationCoordinator > > ,
@@ -394,6 +506,26 @@ fn message_to_dto(message: Message) -> MessageDTO {
394506
395507 let content = match message. content {
396508 MessageContent :: Text ( text) => serde_json:: json!( { "type" : "text" , "text" : text } ) ,
509+ MessageContent :: Multimodal { text, images } => {
510+ let images: Vec < serde_json:: Value > = images
511+ . into_iter ( )
512+ . map ( |img| {
513+ serde_json:: json!( {
514+ "id" : img. id,
515+ "image_path" : img. image_path,
516+ "mime_type" : img. mime_type,
517+ "metadata" : img. metadata,
518+ "has_data_url" : img. data_url. as_ref( ) . is_some_and( |s| !s. is_empty( ) ) ,
519+ } )
520+ } )
521+ . collect ( ) ;
522+
523+ serde_json:: json!( {
524+ "type" : "multimodal" ,
525+ "text" : text,
526+ "images" : images,
527+ } )
528+ }
397529 MessageContent :: ToolResult {
398530 tool_id,
399531 tool_name,
0 commit comments