From 79442b3be8ac85bbcad4b5d4f4125020bda7fbe2 Mon Sep 17 00:00:00 2001 From: Josh Stevenson Date: Sun, 26 Jul 2026 19:14:18 -0500 Subject: [PATCH] feat: unify provider stacks, add 6 Studio types, externalize prompts to TOML - F-06: Add resolve_llm_config() shared function; studio now uses model registry like chat instead of bypassing it with hardcoded fallbacks - F-01: Expose all 16 Studio output types in frontend dropdown (was 10) - F-02: Externalize 16 LLM prompts to prompts/studio_prompts.toml via include_str! and OnceLock; 6 new types get proper prompts instead of generic fallback - F-03: Document custom cloud endpoint opt-in in README provider table - README: Add 4 Mermaid architecture diagrams (retrieval, provider, studio, ingestion) and update Studio section to reflect 16 types 189/189 Rust tests pass, tsc clean, zero warnings. --- .../plans/2026-07-26_studio-provider-dedup.md | 66 +++ README.md | 79 +++- prompts/studio_prompts.toml | 387 ++++++++++++++++++ src-tauri/src/commands/studio.rs | 252 +++--------- src-tauri/src/providers/mod.rs | 59 +++ src/components/studio/StudioPanel.tsx | 14 +- 6 files changed, 661 insertions(+), 196 deletions(-) create mode 100644 .hermes/plans/2026-07-26_studio-provider-dedup.md create mode 100644 prompts/studio_prompts.toml diff --git a/.hermes/plans/2026-07-26_studio-provider-dedup.md b/.hermes/plans/2026-07-26_studio-provider-dedup.md new file mode 100644 index 0000000..d0a1572 --- /dev/null +++ b/.hermes/plans/2026-07-26_studio-provider-dedup.md @@ -0,0 +1,66 @@ +# Gloss Studio Provider Deduplication Plan + +**Goal**: Unify the LLM provider resolution stack between chat and studio paths, eliminating duplicated configuration with diverged defaults. + +**Architecture**: Extract a shared `resolve_llm_config()` function in `providers/mod.rs` that both chat and studio call. Studio keeps its own timeout constants and non-streaming choice — those are intentional. + +**Status**: Council graph `gloss-studio-refactor-council` running async for design review. + +--- + +## Finding Summary (F-06) + +| Axis | Chat | Studio | Problem | +|------|------|--------|---------| +| Provider resolution | `model_registry.get_provider_config_for_model()` | `provider_config_from_db()` directly | Bypasses registry | +| Model fallback | From registry/model list | Hardcoded `"qwen3.5:4b"` | Silent wrong-model risk | +| Temperature | From `generation_temperature` setting (default 0.7) | Hardcoded 0.3 / 0.2 | Ignores user preference | +| `num_ctx` | Dynamic from model's context window | Hardcoded 16384 | May overflow or underuse | +| `max_tokens` | Dynamically computed | Hardcoded 4096 | Not configurable | +| Timeouts | 180s / 168s / 84s | 60s / 60s / 30s | Intentional — keep separate | + +--- + +## Files to Modify + +1. **`src-tauri/src/providers/mod.rs`** — Add shared `resolve_llm_config()` function +2. **`src-tauri/src/commands/studio.rs`** — Replace `run_studio_llm()` provider block (~line 702-720) +3. **`src-tauri/src/commands/chat/mod.rs`** — Optional: refactor to use same shared function (~line 776-799) + +--- + +## Proposed Shared Function + +```rust +// In providers/mod.rs +pub struct ResolvedLlmConfig { + pub config: ProviderConfig, + pub model: String, + pub model_context_window: Option, +} + +/// Resolve provider config and model for LLM calls. +/// Uses the model registry when available, falls back to direct DB lookup. +/// model_override: if Some, use this model; if None, use default_model from settings. +pub fn resolve_llm_config( + app_db: &AppDb, + secret_store: &SecretStore, + model_registry: Option<&ModelRegistry>, + model_override: Option<&str>, +) -> Result { + // ... +} +``` + +## What to Keep Separate + +- **Studio timeouts**: 60s/60s/30s are intentionally shorter — Studio is batch generation, not interactive chat. Keep `STUDIO_*_TIMEOUT` constants. +- **Studio non-streaming**: `stream: false` is correct for structured JSON generation. Keep. +- **Studio max_tokens**: Should be configurable but defaults can differ from chat. + +## Tests to Gate + +- Existing 189 Rust tests must keep passing +- Add: test that `resolve_llm_config()` returns same config as chat's current path +- Add: test that studio generation still works with the unified path +- Run: `cargo test --features semantic-memory-turbo-quant` diff --git a/README.md b/README.md index 367dec5..832bd90 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,77 @@ Gloss is designed around four boundaries: - Evidence is inspectable. Citations, source scope, prompt metadata, decoding settings, retrieval decisions, and generation status are available in the desktop inspector. - Degradation is disclosed. Missing indices, optional tools, or model capabilities produce reason codes, disabled paths, or bounded fallbacks. +## Architecture + +### Retrieval and chat pipeline + +```mermaid +flowchart TD + A[User query] --> B[Multi-angle query rewriting\n5s timeout, graceful fallback] + B --> C[Source scope resolution] + C --> D{Retrieval backends} + D --> E[SQLite FTS5 / BM25] + D --> F[HNSW dense search\nusearch] + E --> G[Reciprocal-rank fusion] + F --> G + G --> H[Optional re-rank / fallback] + H --> I[Context assembly\nbounded prompt window] + I --> J[Provider stream\nOllama / OpenAI / Anthropic / llama.cpp] + J --> K[Citation extraction] + K --> L[Answer + citations + receipts] +``` + +### Provider resolution + +```mermaid +flowchart TD + A[LLM request] --> B{Model specified?} + B -->|Yes| C[resolve_llm_config\nshared chat + studio path] + B -->|No| D[Read default_model\nfrom settings] + D --> C + C --> E{Model registry\navailable?} + E -->|Yes| F[get_provider_config_for_model\nvalidates model exists] + E -->|No| G[provider_config_from_db\nwith default_provider] + F --> H[ProviderConfig + model + context_window] + G --> H + H --> I[build_provider] + I --> J[LlmProvider trait\nchat / health_check] + J --> K{Provider type} + K -->|Ollama/llama.cpp| L[Loopback default\nLAN with opt-in] + K -->|OpenAI/Anthropic| M[Official host default\ncustom cloud with opt-in] +``` + +### Studio generation pipeline + +```mermaid +flowchart TD + A[User selects output type\n16 types available] --> B[Build deterministic artifact\ngenerate_artifact with snippets] + B --> C{LLM refinement\nenabled?} + C -->|No| D[Return deterministic template] + C -->|Yes| E{Widget type?\nflashcards/quiz/mind_map} + E -->|Yes| F[generate_structured_widget_content\nJSON schema validation] + E -->|No| G[refine_studio_artifact\nLLM prompt from TOML templates] + F --> H{Validation passed?} + G --> H + H -->|Yes| I[Return LLM-refined content] + H -->|No| J[Fallback to deterministic template\nStudioFallbackReceipt recorded] + D --> K[Persist + render] + I --> K + J --> K +``` + +### Ingestion pipeline + +```mermaid +flowchart LR + A[Source added] --> B[Extract\ntext/PDF/docx/xlsx/URL/YouTube/audio/video/image] + B --> C[Chunk\nrecursive split, 800-token target] + C --> D[Embed\nNomicEmbedTextV15 via FastEmbed/Candle] + D --> E[Index\nHNSW via usearch] + E --> F[Summarize\nLLM summary with suggested questions] + F --> G[Ready] +``` + ## What works today ### Notebooks and sources @@ -49,7 +120,7 @@ Gloss is designed around four boundaries: - Create, edit, pin, and delete notebook-scoped notes. - Save useful chat responses as notes. -- Generate source-bound reports, summaries, outlines, FAQs, flashcards, quizzes, mind maps, timelines, comparison tables, and action plans from the current source scope. +- Generate source-bound reports, summaries, outlines, FAQs, flashcards, quizzes, mind maps, timelines, comparison tables, action plans, briefing docs, study guides, custom reports, slide decks, infographics, and audio overview scripts from the current source scope. - Export Studio artifacts as JSON with a digest-bearing export receipt. ### Diagnostics and recovery @@ -86,8 +157,8 @@ Archive files and opaque binary/model formats are rejected as ordinary sources. | --- | --- | --- | | Ollama | `http://localhost:11434` | Loopback only | | llama.cpp | `http://localhost:8080/v1` | Loopback only | -| OpenAI | `https://api.openai.com/v1` | Official HTTPS host only | -| Anthropic | `https://api.anthropic.com/v1` | Official HTTPS host only | +| OpenAI | `https://api.openai.com/v1` | Official HTTPS host only; custom endpoints with opt-in | +| Anthropic | `https://api.anthropic.com/v1` | Official HTTPS host only; custom endpoints with opt-in | RFC1918 LAN endpoints for local providers require `allow_lan_local_providers`. Custom OpenAI or Anthropic HTTPS endpoints require `allow_custom_cloud_endpoints`. Provider URLs reject embedded credentials, query strings, and fragments. @@ -255,7 +326,7 @@ The primary runtime ownership map is in [`AGENTS.md`](AGENTS.md). Historical aud - Image, audio, and video paths depend on configured models or optional local tools and are not equally proven across formats. - Audio-overview generation is not a release-proven user workflow. - Semantic-memory and TurboQuant controls are experimental. Gloss-local retrieval remains the stable fallback. -- Studio exposes ten output kinds in the current UI; additional backend artifact kinds are not presented as finished user workflows. +- Studio exposes sixteen output kinds in the current UI with dedicated LLM prompts configured via `prompts/studio_prompts.toml`. - Cloud-provider use sends the assembled request context to that provider. ## Contributing diff --git a/prompts/studio_prompts.toml b/prompts/studio_prompts.toml new file mode 100644 index 0000000..61b211c --- /dev/null +++ b/prompts/studio_prompts.toml @@ -0,0 +1,387 @@ +# Studio prompt templates — one [section] per output type. +# Used by `studio_prompt_for_kind()` in commands/studio.rs. +# {title} and {source_material} are interpolated at call time. + +[report] +system_prompt = """\ +You are an expert analyst writing a source-grounded report. \ +Synthesize the provided sources into a structured report. \ +Use clear section headers, cite specific claims from the sources, \ +and never fabricate information. Your tone is professional and objective.\ +""" +user_prompt = """\ +## Task: Source-Grounded Report + +Title: {title} + +Write a detailed report based EXCLUSIVELY on the following sources. \ +Organize it with clear sections — e.g. Overview, Key Findings, Details, \ +and Recommendations if applicable. \ +Every claim must be traceable back to the sources. + +## Sources + +{source_material}\ +""" + +[summary] +system_prompt = """\ +You are a concise summarizer. Read the provided sources and produce \ +a tight, high-density summary covering only the most important points. \ +Be factual, don't editorialize. Use bullet points for key takeaways.\ +""" +user_prompt = """\ +## Task: Concise Summary + +Title: {title} + +Summarize ALL of the following sources into a concise overview. \ +Capture the essential arguments, facts, and conclusions. \ +Start with a 2-3 sentence overview, then use bullet points for \ +the most important takeaways from each source. Keep it dense. + +## Sources + +{source_material}\ +""" + +[outline] +system_prompt = """\ +You are a structural editor. Build a hierarchical outline from the \ +provided source material. Use nested headings — main topics at the top, \ +subtopics indented underneath. Every heading must correspond to real content \ +in the sources. Don't invent structure where none exists.\ +""" +user_prompt = """\ +## Task: Hierarchical Outline + +Title: {title} + +Build a nested outline from the following sources. \ +Use markdown headings (##, ###, ####) to show the hierarchy. \ +Each heading must reflect real content — don't add fictional topics. + +## Sources + +{source_material}\ +""" + +[faq] +system_prompt = """\ +You are a knowledge curator writing a FAQ. Generate question-answer pairs \ +based on the source material. Questions should anticipate what a reader \ +would genuinely ask. Answers must be factual and cite the relevant source. \ +Format as Q&A.\ +""" +user_prompt = """\ +## Task: FAQ + +Title: {title} + +Generate 5-10 question-answer pairs based on the following sources. \ +Questions should be natural and useful. Answers must be grounded in \ +the source text. Format each as: + +**Q:** [question] +**A:** [answer — cite the source] + +## Sources + +{source_material}\ +""" + +[flashcards] +system_prompt = """\ +You create study flashcards from source material. Each card has a front \ +(question or prompt) and a back (answer). Make them self-contained — \ +someone should be able to test themselves with just these cards.\ +""" +user_prompt = """\ +## Task: Flashcards + +Title: {title} + +Create 8-15 flashcards from the following sources. \ +Format each card as: + +**Front:** [question / concept / term] +**Back:** [answer / definition / explanation] + +## Sources + +{source_material}\ +""" + +[quiz] +system_prompt = """\ +You are a quiz designer. Write multiple-choice quiz questions \ +that test understanding of the source material. Each question needs \ +4 plausible choices with exactly one correct answer.\ +""" +user_prompt = """\ +## Task: Quiz + +Title: {title} + +Write 5-8 multiple-choice quiz questions based on these sources. \ +For each question, provide 4 choices (A-D) and indicate the correct answer. \ +Format as: + +**[N]. Question text** +A. Choice one +B. Choice two +C. Choice three +D. Choice four + +Answer: [letter] + +## Sources + +{source_material}\ +""" + +[mind_map] +system_prompt = """\ +You extract conceptual relationships from source material for mind map \ +visualization. Identify central concepts and their connections. \ +Output a hierarchical concept tree with labeled relationships.\ +""" +user_prompt = """\ +## Task: Mind Map / Concept Tree + +Title: {title} + +Extract the key concepts from these sources and map their relationships. \ +Output as a hierarchical concept tree: + +- **Central concept** (root node) + - Related concept with relationship label + - Sub-concept + - Another branch + +Keep relationships clear and labeled. Every concept must come from the sources. + +## Sources + +{source_material}\ +""" + +[timeline] +system_prompt = """\ +You reconstruct chronological sequences from source material. \ +Extract dated events and arrange them in temporal order. \ +If exact dates are missing, use relative ordering where the text indicates it.\ +""" +user_prompt = """\ +## Task: Timeline + +Title: {title} + +Extract all dated or time-sequenced events from these sources and \ +arrange them chronologically. For each event include: +- Date (approximate if exact unknown) +- Event description +- Source reference + +## Sources + +{source_material}\ +""" + +[compare_table] +system_prompt = """\ +You build comparison tables from source material. Identify comparable \ +entities, dimensions, or arguments and present them in a structured table. \ +Every cell must be source-grounded.\ +""" +user_prompt = """\ +## Task: Comparison Table + +Title: {title} + +Identify the key entities, approaches, or arguments in these sources \ +and build a comparison table. Present as a markdown table with clear \ +column headers. Every value must be traceable to the sources. + +## Sources + +{source_material}\ +""" + +[action_plan] +system_prompt = """\ +You derive actionable plans from source material. Extract recommendations, \ +next steps, or implied actions and organize them into a concrete plan. \ +Each action must be justified by the source text.\ +""" +user_prompt = """\ +## Task: Action Plan + +Title: {title} + +Derive a concrete action plan from these sources. For each action include: +- What needs to be done +- Why (source justification) +- Priority or suggested order + +## Sources + +{source_material}\ +""" + +# ── Six new output types (previously used generic fallback) ── + +[briefing_doc] +system_prompt = """\ +You are a professional analyst creating an executive briefing document. \ +Synthesize the source material into a concise, leadership-ready brief. \ +Include an Executive Summary, Key Findings with supporting evidence, \ +Main Themes, Open Questions, and Recommendations. \ +Cite sources for every factual claim.\ +""" +user_prompt = """\ +## Task: Executive Briefing + +Title: {title} + +Create a briefing document from the following source material. \ +Structure it with these sections: +1. Executive Summary (2-3 sentences) +2. Key Findings (bullet points with source citations) +3. Main Themes +4. Open Questions +5. Recommendations + +Every finding must be grounded in the sources. + +## Sources + +{source_material}\ +""" + +[study_guide] +system_prompt = """\ +You are an educational content designer creating a study guide. \ +Organize the source material into a structured learning resource. \ +Include topic overviews, key concepts with explanations, \ +and review questions. Make it self-contained — a student should \ +be able to learn the material from this guide alone.\ +""" +user_prompt = """\ +## Task: Study Guide + +Title: {title} + +Create a comprehensive study guide from these sources. Structure it with: +1. Learning Objectives +2. Topic-by-topic breakdown with explanations +3. Key Concepts and Definitions +4. Review Questions (with answers) + +Use clear headings and subheadings. Every explanation must be source-grounded. + +## Sources + +{source_material}\ +""" + +[custom_report] +system_prompt = """\ +You are a versatile report writer. Examine the source material and produce \ +the most appropriate report format — this could be a technical analysis, \ +a market overview, a literature review, or another format that fits the content. \ +Be thorough, structured, and always cite your sources.\ +""" +user_prompt = """\ +## Task: Custom Report + +Title: {title} + +Analyze the following sources and produce a well-structured report. \ +Choose the most appropriate format based on the content. Include: +- A clear introduction framing the topic +- Organized sections with descriptive headings +- Evidence-backed claims with source citations +- A conclusion or summary + +Adapt your structure to the material — don't force a rigid template. + +## Sources + +{source_material}\ +""" + +[slide_deck] +system_prompt = """\ +You are a presentation designer. Convert the source material into a \ +structured slide deck outline. Each slide should have a clear title, \ +key bullet points, and speaker notes where helpful. \ +Design for clarity — slides should be scannable at a glance.\ +""" +user_prompt = """\ +## Task: Slide Deck + +Title: {title} + +Create a slide deck outline from these sources. For each slide include: +- Slide number and title +- 3-5 bullet points (concise, scannable) +- Speaker notes (1-2 sentences of elaboration) + +Aim for 8-15 slides depending on source depth. Start with a title slide \ +and end with a summary or next-steps slide. + +## Sources + +{source_material}\ +""" + +[infographic] +system_prompt = """\ +You are an information designer. Extract the most compelling statistics, \ +comparisons, and key facts from the source material and organize them \ +into a structured infographic outline. Focus on visualizable data — \ +numbers, percentages, timelines, comparisons, and hierarchies.\ +""" +user_prompt = """\ +## Task: Infographic Outline + +Title: {title} + +Extract data suitable for an infographic from these sources. Organize into: +1. **Headline stat** — the single most impactful number or fact +2. **Key figures** — 4-6 statistics with labels and source references +3. **Comparison or timeline** — if the data supports it +4. **Bottom line** — one-sentence takeaway + +Focus on quantitative, visualizable information. Every number must be sourced. + +## Sources + +{source_material}\ +""" + +[audio_overview] +system_prompt = """\ +You are an audio script writer. Convert the source material into a \ +conversational audio overview script suitable for text-to-speech. \ +Write in a natural, spoken style — use shorter sentences, avoid \ +markdown formatting, and make it engaging to listen to.\ +""" +user_prompt = """\ +## Task: Audio Overview Script + +Title: {title} + +Write an audio overview script from these sources. Guidelines: +- Conversational tone — write how people speak, not how they read +- Short sentences and natural pauses +- 5-10 minutes of spoken content +- Structure: intro → key points → deeper dive → wrap-up +- No markdown, no bullet points, no visual references +- Cite sources naturally in speech ("According to [source]...") + +## Sources + +{source_material}\ +""" diff --git a/src-tauri/src/commands/studio.rs b/src-tauri/src/commands/studio.rs index 47e0ff6..abf7507 100644 --- a/src-tauri/src/commands/studio.rs +++ b/src-tauri/src/commands/studio.rs @@ -3,6 +3,8 @@ use crate::error::GlossError; use crate::providers::{ build_provider, ChatMessage, ChatRequest, LlmExecutionContext, LlmPhaseTimeouts, }; +use std::collections::HashMap; +use std::sync::OnceLock; use crate::redaction::redact_path; use crate::state::{ActiveStudioAttempt, AppState}; use crate::studio::{ @@ -700,25 +702,29 @@ async fn run_studio_llm( cancellation: CancellationToken, attempt_id: &str, ) -> Result<(String, StudioProviderRuntimeReceipt), StudioGenerationFailure> { - let (config, model) = { + // Resolve provider config through the shared path — same as chat uses. + // This ensures both paths agree on model selection, provider routing, + // and context window detection. + let resolved = { let app_db = state .app_db .lock() .map_err(|e| studio_provider_error(attempt_id, &e.to_string(), 0))?; - let model = app_db - .get_setting("default_model") - .map_err(|e| studio_provider_error(attempt_id, &e.to_string(), 0))? - .unwrap_or_else(|| "qwen3.5:4b".to_string()); - let config = crate::providers::provider_config_from_db(&app_db, &state.secret_store, { - let selected = app_db - .get_setting("default_provider") - .map_err(|e| studio_provider_error(attempt_id, &e.to_string(), 0))? - .and_then(|p| crate::providers::ProviderType::from_str(p.trim())); - selected.unwrap_or(crate::providers::ProviderType::Ollama) - }) - .map_err(|e| studio_provider_error(attempt_id, &e.to_string(), 0))?; - (config, model) + let registry = state + .model_registry + .lock() + .map_err(|e| studio_provider_error(attempt_id, &e.to_string(), 0))?; + crate::providers::resolve_llm_config( + &app_db, + &state.secret_store, + Some(&*registry), + None, // use default_model from settings — no hardcoded fallback + ) + .map_err(|e| studio_provider_error(attempt_id, &e.to_string(), 0))? }; + let config = resolved.config; + let model = resolved.model; + let model_context_window = resolved.model_context_window; let provider_name = config.provider_type.as_str().to_string(); let _llm_permit = state.llm_gate.acquire().await.map_err(|e| { @@ -738,14 +744,14 @@ async fn run_studio_llm( content: user_prompt, images: None, }], - max_tokens: 4096, + max_tokens: 4096, // Studio generates structured content — keep a generous output window temperature, top_p: None, top_k: None, min_p: None, repeat_penalty: None, - stream: false, - num_ctx: Some(16384), + stream: false, // Studio collects full structured JSON, not token-by-token chat + num_ctx: Some(model_context_window.unwrap_or(16384) as u32), }; let start = Instant::now(); @@ -1206,184 +1212,48 @@ async fn generate_structured_widget_content( fn studio_prompt_for_kind( kind: &str, title: &str, - kind_label: &str, + _kind_label: &str, source_material: &str, ) -> (String, String) { - match kind { - "report" => ( - "You are an expert analyst writing a source-grounded report. \ - Synthesize the provided sources into a structured report. \ - Use clear section headers, cite specific claims from the sources, \ - and never fabricate information. Your tone is professional and objective." - .to_string(), - format!( - "## Task: Source-Grounded Report\n\n\ - Title: {title}\n\n\ - Write a detailed report based EXCLUSIVELY on the following sources. \ - Organize it with clear sections — e.g. Overview, Key Findings, Details, \ - and Recommendations if applicable. \ - Every claim must be traceable back to the sources.\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "summary" => ( - "You are a concise summarizer. Read the provided sources and produce \ - a tight, high-density summary covering only the most important points. \ - Be factual, don't editorialize. Use bullet points for key takeaways." - .to_string(), - format!( - "## Task: Concise Summary\n\n\ - Title: {title}\n\n\ - Summarize ALL of the following sources into a concise overview. \ - Capture the essential arguments, facts, and conclusions. \ - Start with a 2-3 sentence overview, then use bullet points for \ - the most important takeaways from each source. Keep it dense.\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "outline" => ( - "You are a structural editor. Build a hierarchical outline from the \ - provided source material. Use nested headings — main topics at the top, \ - subtopics indented underneath. Every heading must correspond to real content \ - in the sources. Don't invent structure where none exists." - .to_string(), - format!( - "## Task: Hierarchical Outline\n\n\ - Title: {title}\n\n\ - Build a nested outline from the following sources. \ - Use markdown headings (##, ###, ####) to show the hierarchy. \ - Each heading must reflect real content — don't add fictional topics.\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "faq" => ( - "You are a knowledge curator writing a FAQ. Generate question-answer pairs \ - based on the source material. Questions should anticipate what a reader \ - would genuinely ask. Answers must be factual and cite the relevant source. \ - Format as Q&A." - .to_string(), - format!( - "## Task: FAQ\n\n\ - Title: {title}\n\n\ - Generate 5-10 question-answer pairs based on the following sources. \ - Questions should be natural and useful. Answers must be grounded in \ - the source text. Format each as:\n\n\ - **Q:** [question]\n\ - **A:** [answer — cite the source]\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "flashcards" => ( - "You create study flashcards from source material. Each card has a front \ - (question or prompt) and a back (answer). Make them self-contained — \ - someone should be able to test themselves with just these cards." - .to_string(), - format!( - "## Task: Flashcards\n\n\ - Title: {title}\n\n\ - Create 8-15 flashcards from the following sources. \ - Format each card as:\n\n\ - **Front:** [question / concept / term]\n\ - **Back:** [answer / definition / explanation]\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "quiz" => ( - "You are a quiz designer. Write multiple-choice quiz questions \ - that test understanding of the source material. Each question needs \ - 4 plausible choices with exactly one correct answer." - .to_string(), - format!( - "## Task: Quiz\n\n\ - Title: {title}\n\n\ - Write 5-8 multiple-choice quiz questions based on these sources. \ - For each question, provide 4 choices (A-D) and indicate the correct answer. \ - Format as:\n\n\ - **[N]. Question text**\n\ - A. Choice one\n\ - B. Choice two\n\ - C. Choice three\n\ - D. Choice four\n\n\ - Answer: [letter]\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "mind_map" => ( - "You extract conceptual relationships from source material for mind map \ - visualization. Identify central concepts and their connections. \ - Output a hierarchical concept tree with labeled relationships." - .to_string(), - format!( - "## Task: Mind Map / Concept Tree\n\n\ - Title: {title}\n\n\ - Extract the key concepts from these sources and map their relationships. \ - Output as a hierarchical concept tree:\n\n\ - - **Central concept** (root node)\n\ - - Related concept with relationship label\n\ - - Sub-concept\n\ - - Another branch\n\n\ - Keep relationships clear and labeled. Every concept must come from the sources.\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "timeline" => ( - "You reconstruct chronological sequences from source material. \ - Extract dated events and arrange them in temporal order. \ - If exact dates are missing, use relative ordering where the text indicates it." - .to_string(), - format!( - "## Task: Timeline\n\n\ - Title: {title}\n\n\ - Extract all dated or time-sequenced events from these sources and \ - arrange them chronologically. For each event include:\n\ - - Date (approximate if exact unknown)\n\ - - Event description\n\ - - Source reference\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "compare_table" => ( - "You build comparison tables from source material. Identify comparable \ - entities, dimensions, or arguments and present them in a structured table. \ - Every cell must be source-grounded." - .to_string(), - format!( - "## Task: Comparison Table\n\n\ - Title: {title}\n\n\ - Identify the key entities, approaches, or arguments in these sources \ - and build a comparison table. Present as a markdown table with clear \ - column headers. Every value must be traceable to the sources.\n\n\ - ## Sources\n\n{source_material}" - ), - ), - "action_plan" => ( - "You derive actionable plans from source material. Extract recommendations, \ - next steps, or implied actions and organize them into a concrete plan. \ - Each action must be justified by the source text." - .to_string(), - format!( - "## Task: Action Plan\n\n\ - Title: {title}\n\n\ - Derive a concrete action plan from these sources. For each action include:\n\ - - What needs to be done\n\ - - Why (source justification)\n\ - - Priority or suggested order\n\n\ - ## Sources\n\n{source_material}" - ), + let prompts = studio_prompts(); + if let Some(tmpl) = prompts.get(kind) { + return ( + tmpl.system_prompt.clone(), + tmpl.user_prompt + .replace("{title}", title) + .replace("{source_material}", source_material), + ); + } + // Generic fallback for unknown output types + let label = kind.replace('_', " "); + ( + format!( + "You produce a well-structured {label} from source material. \ + Be factual, source-grounded, and never invent information." ), - _ => ( - format!( - "You produce a well-structured {kind_label} from source material. \ - Be factual, source-grounded, and never invent information." - ), - format!( - "## Task: {kind_label}\n\n\ - Title: {title}\n\n\ - Produce a {kind_label} based on the following sources:\n\n\ - {source_material}" - ), + format!( + "## Task: {label}\n\n\ + Title: {title}\n\n\ + Produce a {label} based on the following sources:\n\n\ + {source_material}" ), - } + ) +} + +#[derive(Debug, Deserialize, Clone)] +struct StudioPromptTemplate { + system_prompt: String, + user_prompt: String, +} + +static STUDIO_PROMPTS_TOML: &str = include_str!("../../../prompts/studio_prompts.toml"); + +fn studio_prompts() -> &'static HashMap { + static PROMPTS: OnceLock> = OnceLock::new(); + PROMPTS.get_or_init(|| { + toml::from_str(STUDIO_PROMPTS_TOML) + .expect("prompts/studio_prompts.toml is malformed — fix at compile time") + }) } #[cfg(test)] mod tests { diff --git a/src-tauri/src/providers/mod.rs b/src-tauri/src/providers/mod.rs index 44d638b..9bef788 100644 --- a/src-tauri/src/providers/mod.rs +++ b/src-tauri/src/providers/mod.rs @@ -721,6 +721,65 @@ pub fn provider_config_from_db( }) } +/// Resolved LLM configuration for making provider calls. +pub struct ResolvedLlmConfig { + pub config: ProviderConfig, + pub model: String, + pub model_context_window: Option, +} + +/// Resolve provider config and model for LLM calls, shared between chat and studio paths. +/// +/// When `model_registry` and `model_override` are both available, resolves through the +/// registry (which validates the model exists in a cached provider list). Falls back to +/// direct DB lookup via `default_provider` when the registry is unavailable. +/// +/// `model_override` takes priority; when `None`, reads `default_model` from settings. +/// This function never uses a hardcoded model fallback — if no model is configured, +/// it returns an error. +pub fn resolve_llm_config( + app_db: &AppDb, + secret_store: &SecretStore, + model_registry: Option<&ModelRegistry>, + model_override: Option<&str>, +) -> Result { + let model = if let Some(m) = model_override { + m.to_string() + } else { + app_db + .get_setting("default_model")? + .filter(|s| !s.trim().is_empty()) + .ok_or_else(|| { + GlossError::Config( + "No model configured — set a default model in Settings".to_string(), + ) + })? + }; + + let (config, context_window) = if let Some(registry) = model_registry { + let config = registry.get_provider_config_for_model(&model, app_db, secret_store)?; + let context_window = app_db + .get_all_models()? + .into_iter() + .find(|record| record.id == model) + .and_then(|record| record.context_window); + (config, context_window) + } else { + let selected_provider = app_db + .get_setting("default_provider")? + .and_then(|p| ProviderType::from_str(p.trim())) + .unwrap_or(ProviderType::Ollama); + let config = provider_config_from_db(app_db, secret_store, selected_provider)?; + (config, None) + }; + + Ok(ResolvedLlmConfig { + config, + model, + model_context_window: context_window, + }) +} + /// Registry of all configured LLM providers and cached models. #[allow(dead_code)] pub struct ModelRegistry { diff --git a/src/components/studio/StudioPanel.tsx b/src/components/studio/StudioPanel.tsx index 3f09e67..a4b744f 100644 --- a/src/components/studio/StudioPanel.tsx +++ b/src/components/studio/StudioPanel.tsx @@ -1,18 +1,24 @@ import { useEffect, useMemo, useState, type ReactNode } from "react"; import { + BookOpen, ClipboardList, Download, + FileEdit, FileText, GitCompare, + GraduationCap, HelpCircle, ListTree, Map, - OctagonX, Network, + OctagonX, + PieChart, + Presentation, RefreshCw, Rows3, Sparkles, Timer, + Volume2, } from "lucide-react"; import { useSourceStore } from "../../stores/sourceStore"; import { useStudioStore } from "../../stores/studioStore"; @@ -42,6 +48,12 @@ const OUTPUT_TYPES = [ { id: "timeline", label: "Timeline", icon: Timer }, { id: "compare_table", label: "Compare", icon: GitCompare }, { id: "action_plan", label: "Actions", icon: Map }, + { id: "briefing_doc", label: "Briefing", icon: BookOpen }, + { id: "study_guide", label: "Study", icon: GraduationCap }, + { id: "custom_report", label: "Custom", icon: FileEdit }, + { id: "slide_deck", label: "Slides", icon: Presentation }, + { id: "infographic", label: "Infographic", icon: PieChart }, + { id: "audio_overview", label: "Audio", icon: Volume2 }, ] as const; export function StudioPanel({ notebookId }: StudioPanelProps) {