ricecoder_tui/
prompt_context.rs

1//! Prompt context integration with images
2//!
3//! This module provides integration between the prompt input and images,
4//! managing the context that includes both text and image data.
5//!
6//! # Requirements
7//!
8//! - Req 1.4: Add images to prompt context
9//! - Req 5.1: Display images in chat interface
10//! - Req 5.1: Include images in message history
11
12use std::path::PathBuf;
13
14/// Prompt context containing text and images
15///
16/// Manages the context for a prompt including:
17/// - Text input
18/// - Associated images
19/// - Metadata about the context
20///
21/// # Requirements
22///
23/// - Req 1.4: Add images to prompt context
24/// - Req 5.1: Display images in chat interface
25/// - Req 5.1: Include images in message history
26#[derive(Debug, Clone)]
27pub struct PromptContext {
28    /// Text content of the prompt
29    pub text: String,
30    /// Images associated with the prompt
31    pub images: Vec<PathBuf>,
32    /// Whether the context is complete and ready to send
33    pub ready: bool,
34    /// Timestamp when context was created
35    pub created_at: std::time::SystemTime,
36}
37
38impl PromptContext {
39    /// Create a new empty prompt context
40    pub fn new() -> Self {
41        Self {
42            text: String::new(),
43            images: Vec::new(),
44            ready: false,
45            created_at: std::time::SystemTime::now(),
46        }
47    }
48
49    /// Create a new prompt context with text
50    pub fn with_text(text: impl Into<String>) -> Self {
51        Self {
52            text: text.into(),
53            images: Vec::new(),
54            ready: false,
55            created_at: std::time::SystemTime::now(),
56        }
57    }
58
59    /// Create a new prompt context with text and images
60    pub fn with_text_and_images(text: impl Into<String>, images: Vec<PathBuf>) -> Self {
61        Self {
62            text: text.into(),
63            images,
64            ready: false,
65            created_at: std::time::SystemTime::now(),
66        }
67    }
68
69    /// Set the text content
70    ///
71    /// # Arguments
72    ///
73    /// * `text` - The text content
74    pub fn set_text(&mut self, text: impl Into<String>) {
75        self.text = text.into();
76    }
77
78    /// Get the text content
79    pub fn get_text(&self) -> &str {
80        &self.text
81    }
82
83    /// Add an image to the context
84    ///
85    /// # Arguments
86    ///
87    /// * `path` - Path to the image
88    ///
89    /// # Requirements
90    ///
91    /// - Req 1.4: Add images to prompt context
92    pub fn add_image(&mut self, path: PathBuf) {
93        if !self.images.contains(&path) {
94            self.images.push(path);
95        }
96    }
97
98    /// Add multiple images to the context
99    ///
100    /// # Arguments
101    ///
102    /// * `paths` - Paths to the images
103    ///
104    /// # Requirements
105    ///
106    /// - Req 1.4: Add images to prompt context
107    pub fn add_images(&mut self, paths: Vec<PathBuf>) {
108        for path in paths {
109            self.add_image(path);
110        }
111    }
112
113    /// Remove an image from the context
114    ///
115    /// # Arguments
116    ///
117    /// * `path` - Path to the image to remove
118    ///
119    /// # Returns
120    ///
121    /// True if image was removed, false if not found
122    pub fn remove_image(&mut self, path: &PathBuf) -> bool {
123        if let Some(pos) = self.images.iter().position(|p| p == path) {
124            self.images.remove(pos);
125            true
126        } else {
127            false
128        }
129    }
130
131    /// Clear all images from the context
132    pub fn clear_images(&mut self) {
133        self.images.clear();
134    }
135
136    /// Get the images in the context
137    pub fn get_images(&self) -> &[PathBuf] {
138        &self.images
139    }
140
141    /// Get the number of images in the context
142    pub fn image_count(&self) -> usize {
143        self.images.len()
144    }
145
146    /// Check if the context has any images
147    pub fn has_images(&self) -> bool {
148        !self.images.is_empty()
149    }
150
151    /// Check if the context has text
152    pub fn has_text(&self) -> bool {
153        !self.text.is_empty()
154    }
155
156    /// Check if the context is complete (has text or images)
157    pub fn is_complete(&self) -> bool {
158        self.has_text() || self.has_images()
159    }
160
161    /// Mark the context as ready to send
162    pub fn mark_ready(&mut self) {
163        self.ready = true;
164    }
165
166    /// Mark the context as not ready
167    pub fn mark_not_ready(&mut self) {
168        self.ready = false;
169    }
170
171    /// Check if the context is ready to send
172    pub fn is_ready(&self) -> bool {
173        self.ready && self.is_complete()
174    }
175
176    /// Clear the context (text and images)
177    pub fn clear(&mut self) {
178        self.text.clear();
179        self.images.clear();
180        self.ready = false;
181        self.created_at = std::time::SystemTime::now();
182    }
183
184    /// Get a summary of the context
185    pub fn summary(&self) -> String {
186        let mut parts = Vec::new();
187
188        if self.has_text() {
189            parts.push(format!("Text: {} chars", self.text.len()));
190        }
191
192        if self.has_images() {
193            parts.push(format!("Images: {}", self.image_count()));
194        }
195
196        if parts.is_empty() {
197            "Empty context".to_string()
198        } else {
199            parts.join(", ")
200        }
201    }
202}
203
204impl Default for PromptContext {
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_prompt_context_creation() {
216        let context = PromptContext::new();
217        assert_eq!(context.text, "");
218        assert_eq!(context.images.len(), 0);
219        assert!(!context.ready);
220    }
221
222    #[test]
223    fn test_prompt_context_with_text() {
224        let context = PromptContext::with_text("Hello, world!");
225        assert_eq!(context.text, "Hello, world!");
226        assert_eq!(context.images.len(), 0);
227    }
228
229    #[test]
230    fn test_prompt_context_with_text_and_images() {
231        let images = vec![
232            PathBuf::from("/path/to/image1.png"),
233            PathBuf::from("/path/to/image2.jpg"),
234        ];
235        let context = PromptContext::with_text_and_images("Hello", images.clone());
236
237        assert_eq!(context.text, "Hello");
238        assert_eq!(context.images.len(), 2);
239        assert_eq!(context.images, images);
240    }
241
242    #[test]
243    fn test_set_text() {
244        let mut context = PromptContext::new();
245        context.set_text("New text");
246        assert_eq!(context.get_text(), "New text");
247    }
248
249    #[test]
250    fn test_add_image() {
251        let mut context = PromptContext::new();
252        let path = PathBuf::from("/path/to/image.png");
253
254        context.add_image(path.clone());
255        assert_eq!(context.image_count(), 1);
256        assert!(context.has_images());
257        assert_eq!(context.get_images()[0], path);
258    }
259
260    #[test]
261    fn test_add_duplicate_image() {
262        let mut context = PromptContext::new();
263        let path = PathBuf::from("/path/to/image.png");
264
265        context.add_image(path.clone());
266        context.add_image(path.clone());
267
268        // Should not add duplicate
269        assert_eq!(context.image_count(), 1);
270    }
271
272    #[test]
273    fn test_add_multiple_images() {
274        let mut context = PromptContext::new();
275        let paths = vec![
276            PathBuf::from("/path/to/image1.png"),
277            PathBuf::from("/path/to/image2.jpg"),
278        ];
279
280        context.add_images(paths.clone());
281        assert_eq!(context.image_count(), 2);
282    }
283
284    #[test]
285    fn test_remove_image() {
286        let mut context = PromptContext::new();
287        let path = PathBuf::from("/path/to/image.png");
288
289        context.add_image(path.clone());
290        assert_eq!(context.image_count(), 1);
291
292        let removed = context.remove_image(&path);
293        assert!(removed);
294        assert_eq!(context.image_count(), 0);
295    }
296
297    #[test]
298    fn test_remove_image_not_found() {
299        let mut context = PromptContext::new();
300        let path = PathBuf::from("/path/to/image.png");
301
302        let removed = context.remove_image(&path);
303        assert!(!removed);
304    }
305
306    #[test]
307    fn test_clear_images() {
308        let mut context = PromptContext::new();
309        context.add_images(vec![
310            PathBuf::from("/path/to/image1.png"),
311            PathBuf::from("/path/to/image2.jpg"),
312        ]);
313
314        assert_eq!(context.image_count(), 2);
315        context.clear_images();
316        assert_eq!(context.image_count(), 0);
317    }
318
319    #[test]
320    fn test_has_text() {
321        let mut context = PromptContext::new();
322        assert!(!context.has_text());
323
324        context.set_text("Some text");
325        assert!(context.has_text());
326    }
327
328    #[test]
329    fn test_has_images() {
330        let mut context = PromptContext::new();
331        assert!(!context.has_images());
332
333        context.add_image(PathBuf::from("/path/to/image.png"));
334        assert!(context.has_images());
335    }
336
337    #[test]
338    fn test_is_complete() {
339        let mut context = PromptContext::new();
340        assert!(!context.is_complete());
341
342        context.set_text("Some text");
343        assert!(context.is_complete());
344
345        context.clear();
346        assert!(!context.is_complete());
347
348        context.add_image(PathBuf::from("/path/to/image.png"));
349        assert!(context.is_complete());
350    }
351
352    #[test]
353    fn test_ready_state() {
354        let mut context = PromptContext::new();
355        assert!(!context.is_ready());
356
357        context.mark_ready();
358        assert!(!context.is_ready()); // Still not ready because context is empty
359
360        context.set_text("Some text");
361        assert!(context.is_ready());
362
363        context.mark_not_ready();
364        assert!(!context.is_ready());
365    }
366
367    #[test]
368    fn test_clear() {
369        let mut context = PromptContext::new();
370        context.set_text("Some text");
371        context.add_image(PathBuf::from("/path/to/image.png"));
372        context.mark_ready();
373
374        assert!(context.has_text());
375        assert!(context.has_images());
376        assert!(context.ready);
377
378        context.clear();
379
380        assert!(!context.has_text());
381        assert!(!context.has_images());
382        assert!(!context.ready);
383    }
384
385    #[test]
386    fn test_summary() {
387        let mut context = PromptContext::new();
388        assert_eq!(context.summary(), "Empty context");
389
390        context.set_text("Hello, world!");
391        assert!(context.summary().contains("Text"));
392
393        context.add_image(PathBuf::from("/path/to/image.png"));
394        let summary = context.summary();
395        assert!(summary.contains("Text"));
396        assert!(summary.contains("Images"));
397    }
398
399    #[test]
400    fn test_image_count() {
401        let mut context = PromptContext::new();
402        assert_eq!(context.image_count(), 0);
403
404        context.add_images(vec![
405            PathBuf::from("/path/to/image1.png"),
406            PathBuf::from("/path/to/image2.jpg"),
407            PathBuf::from("/path/to/image3.gif"),
408        ]);
409
410        assert_eq!(context.image_count(), 3);
411    }
412
413    #[test]
414    fn test_get_images() {
415        let mut context = PromptContext::new();
416        let paths = vec![
417            PathBuf::from("/path/to/image1.png"),
418            PathBuf::from("/path/to/image2.jpg"),
419        ];
420
421        context.add_images(paths.clone());
422        assert_eq!(context.get_images(), paths.as_slice());
423    }
424}