(
base_url: &str,
model: &str,
api_key: Option<&str>,
include_usage: bool,
history: Vec<Message>,
on_token: &Channel<String>,
cancel_token: &CancellationToken,
)
| 375 | } |
| 376 | |
| 377 | async fn send_openai_compatible( |
| 378 | base_url: &str, |
| 379 | model: &str, |
| 380 | api_key: Option<&str>, |
| 381 | include_usage: bool, |
| 382 | history: Vec<Message>, |
| 383 | on_token: &Channel<String>, |
| 384 | cancel_token: &CancellationToken, |
| 385 | ) -> AppResult<(String, Option<TokenUsage>)> { |
| 386 | let client = http_client::streaming_client()?; |
| 387 | let endpoint = format!("{}/chat/completions", base_url.trim_end_matches('/')); |
| 388 | |
| 389 | let messages: Vec<Value> = history |
| 390 | .iter() |
| 391 | .map(|m| serde_json::json!({ "role": m.role, "content": m.content })) |
| 392 | .collect(); |
| 393 | |
| 394 | let mut payload = serde_json::json!({ |
| 395 | "model": model, |
| 396 | "messages": messages, |
| 397 | "temperature": 0.2, |
| 398 | "presence_penalty": 0.0, |
| 399 | "frequency_penalty": 0.0, |
| 400 | "stream": true, |
| 401 | }); |
| 402 | |
| 403 | if include_usage { |
| 404 | payload["stream_options"] = serde_json::json!({ "include_usage": true }); |
| 405 | } |
| 406 | |
| 407 | // Lazy system prompt: only inject full preview guide when user asks for visuals |
| 408 | let last_user_content = history.iter().rev().find(|m| m.role == "user").map(|m| m.content.as_str()).unwrap_or(""); |
| 409 | let system_instructions = if needs_visual_guide(last_user_content) { |
| 410 | concat!( |
| 411 | "IMPORTANT: Reply using the same language as the user's latest message. If user writes Indonesian, answer in Indonesian. Never switch to another language unless the user explicitly asks you to.\n\n", |
| 412 | "INTERACTIVE PREVIEW: When the user asks for a visualization, diagram, chart, interactive demo, or any visual HTML content, output it as a fenced code block with tag `html:preview`. The app renders it as a live iframe preview with a full design system pre-loaded (CSS variables, SVG color ramp classes, pre-styled form elements, light/dark mode).\n\n", |
| 413 | "Design rules: flat (no gradients/shadows/glow), use CSS vars for colors (var(--color-text-primary), var(--color-background-secondary), etc). system-ui font, 2 weights (400/500), sentence case. Structure: style → content → script last.\n\n", |
| 414 | "SVG diagrams: use pre-loaded classes — `.t` (14px text), `.ts` (12px), `.th` (14px bold), `.box` (neutral), `.node` (clickable), `.arr` (arrow), `.leader` (dashed). Color ramps: `class=\"c-blue\"` on `<g>` wrapping shape+text — auto light/dark. Available: c-purple, c-teal, c-coral, c-blue, c-amber, c-green, c-red, c-gray, c-pink. Max 2-3 ramps per diagram.\n\n", |
| 415 | "Chart.js: wrap canvas in div with position:relative + explicit height. Load UMD from cdnjs.cloudflare.com with onload callback. Disable default legend, build custom HTML legend with 10px colored squares.\n\n", |
| 416 | "Interactive: form elements pre-styled. Use sendPrompt(text) for drill-down. CDN: cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com, esm.sh only.\n\n", |
| 417 | "Always output COMPLETE standalone HTML (DOCTYPE, html, head, body). No titles/prose inside widget — explanations go in your response text." |
| 418 | ) |
| 419 | } else { |
| 420 | concat!( |
| 421 | "IMPORTANT: Reply using the same language as the user's latest message. If user writes Indonesian, answer in Indonesian. Never switch to another language unless the user explicitly asks you to.\n\n", |
| 422 | "You can create interactive visualizations (charts, diagrams, widgets) by outputting a fenced code block with the language tag `html:preview`. The preview iframe has a full design system pre-loaded with CSS variables, SVG color ramp classes, and light/dark mode support. Use this when the user asks for any visual or interactive content." |
| 423 | ) |
| 424 | }; |
| 425 | let payload_with_system = if let Some(arr) = payload.get("messages").and_then(Value::as_array) { |
| 426 | let mut updated = arr.clone(); |
| 427 | updated.insert( |
| 428 | 0, |
| 429 | serde_json::json!({ |
| 430 | "role": "system", |
| 431 | "content": system_instructions, |
| 432 | }), |
| 433 | ); |
| 434 | let mut p = payload.clone(); |
no test coverage detected