From cad3f4b6991140f59f0edeab6f4dedb53b0261db Mon Sep 17 00:00:00 2001 From: vegard Date: Wed, 18 Mar 2026 06:13:09 +0000 Subject: [PATCH] =?UTF-8?q?Fullf=C3=B8rer=20oppgave=2018.1:=20AI-preset=20?= =?UTF-8?q?node-type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementerer node_kind 'ai_preset' med metadata-validering og 8 standardprompter som seed-data. Validering i maskinrommet (create_node + update_node): - prompt (påkrevd, ikke-tom streng) - model_profile (flash | standard) - category (standard | custom) - default_direction (tool_to_node | node_to_tool | both) - icon (påkrevd, ikke-tom streng) - color (påkrevd, hex-farge #RRGGBB) Seed-presets: Rens tekst, Korrektør, Oppsummering, Oversett, Skriv om for publisering, Trekk ut fakta, Forenkle, Endre tone. 8 enhetstester for valideringsfunksjonen. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/features/ai_verktoy.md | 250 +++++++++++++++++++++++++++++ docs/primitiver/nodes.md | 3 + maskinrommet/src/intentions.rs | 167 +++++++++++++++++++ migrations/015_ai_preset_seeds.sql | 168 +++++++++++++++++++ tasks.md | 3 +- 5 files changed, 589 insertions(+), 2 deletions(-) create mode 100644 docs/features/ai_verktoy.md create mode 100644 migrations/015_ai_preset_seeds.sql diff --git a/docs/features/ai_verktoy.md b/docs/features/ai_verktoy.md new file mode 100644 index 0000000..9aaf9e6 --- /dev/null +++ b/docs/features/ai_verktoy.md @@ -0,0 +1,250 @@ +# Feature: AI-verktøy (Drag-and-drop tekstbehandling) +**Filsti:** `docs/features/ai_verktoy.md` + +## 1. Konsept +Et frittstående verktøy-panel på arbeidsflaten for AI-drevet +tekstbehandling. I stedet for en ✨-knapp inne i editoren er AI-et +et eget verktøy som lever side ved side med chat, artikkelverktøy, +kanban og andre paneler. Brukeren konfigurerer verktøyet med en prompt +og drar innhold til/fra det. + +Inspirert av fysiske verktøy: du har en maskin på arbeidsbenken, +konfigurert til å gjøre én ting godt. Du mater inn materiale, og +får bearbeidet materiale tilbake. Maskinen står der klar mellom bruk. + +## 2. Brukeropplevelse + +### 2.1 Åpne og konfigurere +1. Brukeren trekker AI-verktøyet inn på arbeidsflaten (fra verktøy-paletten). +2. Verktøyet åpner seg med: + - En **prompt-velger** med gjennomarbeidede standardprompter + - Et **fritekst-felt** for egendefinert prompt + - En **modell-indikator** (viser hvilken modell som brukes — styrt av server) +3. Brukeren velger prompt og verktøyet er klart. +4. Verktøyet *husker* konfigurasjonen — det står klart til bruk uten + å rekonfigurere hver gang. + +### 2.2 To retninger — to semantikker + +| Retning | Metafor | Handling | Resultat | +|---|---|---|---| +| **Verktøy → Node** | "Penselen" | Dra AI-verktøyet og slipp på en tekstnode | In-place revisjon: teksten modifiseres, originalen versjoneres | +| **Node → Verktøy** | "Kverna" | Dra en tekstnode og slipp på AI-verktøyet | Ny node opprettes med `derived_from`-edge til kilden | + +**Penselen** — du bruker verktøyet *på* teksten. Passer for korrektur, +språkvask, rensing av innlimt rot. Teksten oppgraderes, originalen +bevares som revisjon. + +**Kverna** — du sender teksten *gjennom* verktøyet. Passer for +oppsummering, oversettelse, uttrekk av fakta. Output er et nytt +objekt som kan tas med videre til andre verktøy på flaten. + +### 2.3 Typisk bruksscenario +``` +Vegard sitter på bussen, leser en artikkel i VG på mobilen. +Ctrl+A → kopierer alt (artikkel + ingresser + annonser + navigasjon). +Limer inn i en ny node i Synops. + +Drar AI-verktøyet (konfigurert med "Rens tekst") → noden. +AI-et vasker bort alt som ikke er artikkelen, strukturerer teksten, +forkorter, og oppgir kilde. Originalen er bevart som revisjon. + +Ferdig. Ren, lesbar tekst. Ingen rot. +``` + +## 3. Standardprompter + +Gjennomarbeidede, klare til bruk uten konfigurasjon: + +| Prompt | Hva den gjør | Typisk retning | +|---|---|---| +| **Rens tekst** | Fjerner støy fra innlimt webinnhold, fikser skrivefeil, strukturerer, oppgir kilde | Verktøy → Node | +| **Korrektør** | Fikser grammatikk og skrivefeil, beholder innhold intakt | Verktøy → Node | +| **Oppsummering** | Kondenserer til 2–5 setninger med det viktigste | Node → Verktøy | +| **Oversett** | Oversetter til/fra norsk/engelsk | Begge | +| **Skriv om for publisering** | Omskriver til artikkelformat med tittel, ingress, brødtekst | Node → Verktøy | +| **Trekk ut fakta** | Identifiserer påstander, tall, sitater som separate punkter | Node → Verktøy | +| **Forenkle** | Omskriver til klarere, enklere språk | Verktøy → Node | +| **Endre tone** | Formell ↔ uformell, saklig ↔ engasjert | Verktøy → Node | + +Brukere kan lage egne prompter og lagre dem. Egendefinerte prompter +lagres som noder (`node_kind: 'ai_preset'`) som kan deles via edges. + +## 4. Modellstyring + +**Modell bestemmes av serveren, ikke brukeren.** + +Hvert AI-verktøy (standardprompt eller egendefinert) har en +server-tildelt modellprofil. Brukeren ser hvilken modell som brukes +(transparens), men kan ikke endre den. + +| Prompt-kategori | Modellprofil | Begrunnelse | +|---|---|---| +| Rens tekst, korrektør, forenkle | Flash (billig, rask) | Enkle transformasjoner | +| Oppsummering, oversettelse | Flash (billig, rask) | Rutineoppgaver | +| Skriv om for publisering | Standard | Krever mer resonering | +| Trekk ut fakta | Standard | Nøyaktighet viktigere | +| Egendefinert (default) | Flash | Kan oppgraderes av admin | + +Modellprofiler konfigureres i maskinrommet og mappes til LiteLLM-aliaser. +Admin kan justere modellprofil per prompt via Prompt Lab. + +**Kostnadskontroll:** Serveren styrer modellvalg for å hindre at brukere +velger den dyreste modellen for alt. AI-forbruk logges per bruker/samling +i `ai_usage_log`. + +## 5. AI-verktøyet som node + +AI-verktøyet er selv en node i grafen (`node_kind: 'ai_preset'`): + +```jsonc +{ + "node_kind": "ai_preset", + "title": "Rens tekst", + "metadata": { + "prompt": "Fiks denne teksten. Output på norsk...", + "model_profile": "flash", + "category": "standard", // "standard" | "custom" + "default_direction": "tool_to_node", // hint for UI + "icon": "sparkles", + "color": "#8B5CF6" + } +} +``` + +Fordeler: +- **Delbart:** Edge til samling/team → alle i teamet har tilgang +- **Versjonerbart:** Promptjustering over tid med revisjonshistorikk +- **Sporbart:** `derived_from`-edge fra output til kilde + `processed_by`-edge + til AI-preset → full provenance i kunnskapsgrafen +- **Gjenbrukbart:** Flere instanser av samme verktøy med ulik konfigurasjon + +## 6. Teknisk arkitektur + +### 6.1 Signalflyt +``` +Bruker drar node → AI-verktøy (eller omvendt) + │ + ▼ +Frontend sender intention til maskinrommet: + POST /intentions/ai_process + { + source_node_id: "...", + ai_preset_id: "...", + direction: "node_to_tool" | "tool_to_node" + } + │ + ▼ +Maskinrommet: + 1. Hent kilde-node content + 2. Hent AI-preset prompt + modellprofil + 3. Map modellprofil → LiteLLM-alias + 4. Send til AI Gateway (LiteLLM) + 5a. direction = tool_to_node: + → Lagre original som revisjon + → Oppdater node content med AI-output + 5b. direction = node_to_tool: + → Opprett ny node med AI-output + → Opprett derived_from-edge til kilde + → Opprett processed_by-edge til AI-preset + 6. Logg forbruk i ai_usage_log + │ + ▼ +Frontend mottar oppdatering via SpacetimeDB +``` + +### 6.2 Drag-and-drop integrasjon + +AI-verktøyet følger arbeidsflate-konvensjonene fra +`docs/retninger/arbeidsflaten.md`: + +**Som mål (node → verktøy):** +- Aksepterer: alle noder med `content`-felt (tekst, chatmeldinger, notater) +- Drop-sone lyser opp med verktøyets farge under drag +- Ved drop: opprett ny node, vis den ved siden av verktøyet + +**Som kilde (verktøy → node):** +- Verktøyet kan "dras" til en tekstnode +- Drop-sone på tekstnoden lyser opp +- Ved drop: modifiser noden in-place + +**Inkompatibilitet:** +- Lydfiler, bilder, mediefiler → "AI-verktøyet behandler kun tekst" +- Tomme noder → "Noden har ikke innhold å behandle" + +### 6.3 Oppdatering av kompatibilitetsmatrisen + +Legges til i `docs/retninger/arbeidsflaten.md`: + +| Kilde → Mål | AI-verktøy | +|---|---| +| **Chatboble** | Ny node med AI-output + `derived_from` | +| **Tekst/notat** | Ny node med AI-output + `derived_from` | +| **Lydfil** | Inkompatibel: "AI-verktøyet behandler kun tekst" | +| **Transkripsjon** | Ny node med AI-output + `derived_from` | +| **Bilde** | Inkompatibel | + +| Kilde → Mål | Tekstnode (fra AI-verktøy) | +|---|---| +| **AI-verktøy** | In-place revisjon av tekstnoden | + +## 7. Forhold til eksisterende AI-design + +### Erstatter ✨-knappen i editoren +Editor-proposalen (`docs/proposals/editor.md`) beskriver en ✨-knapp +inne i editoren. Med spatial workspace-retningen erstattes denne av +AI-verktøyet som eget panel. Fordeler: + +- **Separat konfigurasjonsflate:** Prompt og parametere lever i sitt + eget panel, ikke gjemt bak en meny i editoren +- **Persistent:** Verktøyet står konfigurert og klart. ✨-knappen + krever prompt-valg hver gang +- **Konsistent drag-and-drop:** Samme interaksjonsmønster som mellom + alle andre verktøy på flaten +- **Flere samtidige:** Du kan ha "Rens tekst" og "Oppsummer" som to + separate paneler, begge klare + +### Bygger på Prompt Lab +Egendefinerte prompter kan opprettes og testes i Prompt Lab +(`docs/features/prompt_lab.md`) før de deployes som AI-preset-noder. + +### Fremtidig: kjeding +Flere AI-verktøy i serie: dra output fra "Oversett" videre til +"Forenkle". Grafen sporer hele transformasjonskjeden via +`derived_from`-edges. Ikke i scope for v1. + +## 8. Avgrensning +- Opererer på **hele noden** (hele `content`-feltet), ikke markert utvalg +- Brukeren kan **ikke** velge modell — kun server-tildelt modellprofil +- Ingen sanntids-streaming av AI-output (synkron respons via jobbkø) +- Støtter kun tekst — ikke bilder, lyd eller video +- Kjeding av verktøy er fremtidig funksjonalitet + +## 9. Utviklingsfaser + +### Fase A: Grunnleggende verktøy-panel +- [x] AI-preset node-type (`node_kind: 'ai_preset'`) + metadata-skjema +- [x] Standard-presets som seed-data (rens tekst, korrektør, oppsummering osv.) +- [ ] `POST /intentions/ai_process` endepunkt i maskinrommet +- [ ] Verktøy-panel UI med prompt-velger og modell-indikator +- [ ] Jobbkø-integrasjon med AI Gateway + +### Fase B: Drag-and-drop +- [ ] Node → verktøy: opprett ny node med `derived_from`-edge +- [ ] Verktøy → node: in-place revisjon med versjonering +- [ ] Drop-sone feedback (farge, inkompatibilitet) +- [ ] Visuell output: ny node animeres inn ved siden av verktøyet + +### Fase C: Egendefinerte prompter +- [ ] Opprett egne AI-preset-noder +- [ ] Del via edges (samling/team) +- [ ] Prompt Lab-integrasjon for testing + +## 10. Instruks for Claude Code +- AI-behandling skjer **alltid** via maskinrommet, aldri direkte fra frontend +- Modellprofil → LiteLLM-alias-mapping konfigureres i maskinrommet +- Brukeren kan **ikke** velge modell — dette er en server-side beslutning +- Standardprompter er seed-data som opprettes ved deploy, ikke hardkodet i frontend +- `derived_from`-edge og `processed_by`-edge gir sporbarhet i grafen +- AI-forbruk logges alltid i `ai_usage_log` per bruker, samling og preset +- Hele noden prosesseres — ingen "markert utvalg"-støtte diff --git a/docs/primitiver/nodes.md b/docs/primitiver/nodes.md index 74890b3..31b2cb8 100644 --- a/docs/primitiver/nodes.md +++ b/docs/primitiver/nodes.md @@ -48,6 +48,9 @@ Kjente node_kinds: | `communication` | Møte, samtale, livesending | | `topic` | Kunnskapsgraf-entitet (Jonas Gahr Støre, Skolepolitikk) | | `media` | CAS-node (lydfil, bilde, video) | +| `agent` | AI-agent (Claude, system) | +| `system_announcement` | Systemvarsler | +| `ai_preset` | AI-verktøy-preset (prompt, modellprofil, kategori) | Listen vokser organisk etter behov. diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index 1d656b7..3e1d3f7 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -102,6 +102,92 @@ fn validate_collection_traits( Ok(()) } +/// Gyldige modellprofiler for AI-presets. +/// Ref: docs/infra/ai_gateway.md § "Modellprofiler" +const VALID_MODEL_PROFILES: &[&str] = &["flash", "standard"]; + +/// Gyldige kategorier for AI-presets. +const VALID_AI_PRESET_CATEGORIES: &[&str] = &["standard", "custom"]; + +/// Gyldige retninger for AI-presets. +const VALID_AI_PRESET_DIRECTIONS: &[&str] = &["tool_to_node", "node_to_tool", "both"]; + +/// Validerer metadata for `node_kind == "ai_preset"`. +/// +/// Påkrevde felter i metadata: +/// - `prompt` (string, ikke tom) +/// - `model_profile` (string, må finnes i VALID_MODEL_PROFILES) +/// - `category` (string, må finnes i VALID_AI_PRESET_CATEGORIES) +/// - `default_direction` (string, må finnes i VALID_AI_PRESET_DIRECTIONS) +/// - `icon` (string, ikke tom) +/// - `color` (string, hex-farge) +/// +/// Ref: docs/features/ai_verktoy.md § 5 +fn validate_ai_preset_metadata( + node_kind: &str, + metadata: &serde_json::Value, +) -> Result<(), String> { + if node_kind != "ai_preset" { + return Ok(()); + } + + // Påkrevd: prompt (ikke-tom streng) + match metadata.get("prompt").and_then(|v| v.as_str()) { + None | Some("") => return Err("ai_preset krever metadata.prompt (ikke-tom streng)".into()), + _ => {} + } + + // Påkrevd: model_profile + match metadata.get("model_profile").and_then(|v| v.as_str()) { + None => return Err("ai_preset krever metadata.model_profile".into()), + Some(p) if !VALID_MODEL_PROFILES.contains(&p) => { + return Err(format!( + "Ugyldig model_profile: '{p}'. Gyldige verdier: {VALID_MODEL_PROFILES:?}" + )); + } + _ => {} + } + + // Påkrevd: category + match metadata.get("category").and_then(|v| v.as_str()) { + None => return Err("ai_preset krever metadata.category".into()), + Some(c) if !VALID_AI_PRESET_CATEGORIES.contains(&c) => { + return Err(format!( + "Ugyldig category: '{c}'. Gyldige verdier: {VALID_AI_PRESET_CATEGORIES:?}" + )); + } + _ => {} + } + + // Påkrevd: default_direction + match metadata.get("default_direction").and_then(|v| v.as_str()) { + None => return Err("ai_preset krever metadata.default_direction".into()), + Some(d) if !VALID_AI_PRESET_DIRECTIONS.contains(&d) => { + return Err(format!( + "Ugyldig default_direction: '{d}'. Gyldige verdier: {VALID_AI_PRESET_DIRECTIONS:?}" + )); + } + _ => {} + } + + // Påkrevd: icon (ikke-tom streng) + match metadata.get("icon").and_then(|v| v.as_str()) { + None | Some("") => return Err("ai_preset krever metadata.icon (ikke-tom streng)".into()), + _ => {} + } + + // Påkrevd: color (hex-farge) + match metadata.get("color").and_then(|v| v.as_str()) { + None | Some("") => return Err("ai_preset krever metadata.color (hex-farge)".into()), + Some(c) if !c.starts_with('#') || c.len() != 7 => { + return Err(format!("Ugyldig color: '{c}'. Forventet hex-farge (#RRGGBB)")); + } + _ => {} + } + + Ok(()) +} + #[derive(Serialize)] pub struct ErrorResponse { pub error: String, @@ -433,6 +519,9 @@ pub async fn create_node( // -- Valider traits for samlingsnoder (oppgave 13.1) -- validate_collection_traits(&node_kind, &metadata).map_err(|e| bad_request(&e))?; + // -- Valider metadata for AI-presets (oppgave 18.1) -- + validate_ai_preset_metadata(&node_kind, &metadata).map_err(|e| bad_request(&e))?; + let metadata_str = metadata.to_string(); // -- Kontekstbasert identitet (oppgave 8.2) -- @@ -868,6 +957,9 @@ pub async fn update_node( // -- Valider traits for samlingsnoder (oppgave 13.1) -- validate_collection_traits(&node_kind, &metadata).map_err(|e| bad_request(&e))?; + // -- Valider metadata for AI-presets (oppgave 18.1) -- + validate_ai_preset_metadata(&node_kind, &metadata).map_err(|e| bad_request(&e))?; + // -- Sjekk om custom_domain er endret (for re-rendering) -- let new_domain = metadata .get("traits") @@ -4319,6 +4411,81 @@ mod tests { assert!(validate_collection_traits("person", &meta).is_ok()); } + // -- AI-preset validering (oppgave 18.1) -- + + fn valid_ai_preset_meta() -> serde_json::Value { + json!({ + "prompt": "Fiks teksten", + "model_profile": "flash", + "category": "standard", + "default_direction": "tool_to_node", + "icon": "sparkles", + "color": "#8B5CF6" + }) + } + + #[test] + fn test_validate_ai_preset_ok() { + let meta = valid_ai_preset_meta(); + assert!(validate_ai_preset_metadata("ai_preset", &meta).is_ok()); + } + + #[test] + fn test_validate_ai_preset_skips_other_kinds() { + // Ugyldig metadata skal ignoreres for andre node_kinds + let meta = json!({}); + assert!(validate_ai_preset_metadata("content", &meta).is_ok()); + assert!(validate_ai_preset_metadata("collection", &meta).is_ok()); + } + + #[test] + fn test_validate_ai_preset_missing_prompt() { + let mut meta = valid_ai_preset_meta(); + meta.as_object_mut().unwrap().remove("prompt"); + let err = validate_ai_preset_metadata("ai_preset", &meta).unwrap_err(); + assert!(err.contains("prompt"), "Feilmelding: {err}"); + } + + #[test] + fn test_validate_ai_preset_empty_prompt() { + let mut meta = valid_ai_preset_meta(); + meta["prompt"] = json!(""); + let err = validate_ai_preset_metadata("ai_preset", &meta).unwrap_err(); + assert!(err.contains("prompt"), "Feilmelding: {err}"); + } + + #[test] + fn test_validate_ai_preset_invalid_model_profile() { + let mut meta = valid_ai_preset_meta(); + meta["model_profile"] = json!("ultra"); + let err = validate_ai_preset_metadata("ai_preset", &meta).unwrap_err(); + assert!(err.contains("model_profile"), "Feilmelding: {err}"); + } + + #[test] + fn test_validate_ai_preset_invalid_category() { + let mut meta = valid_ai_preset_meta(); + meta["category"] = json!("premium"); + let err = validate_ai_preset_metadata("ai_preset", &meta).unwrap_err(); + assert!(err.contains("category"), "Feilmelding: {err}"); + } + + #[test] + fn test_validate_ai_preset_invalid_direction() { + let mut meta = valid_ai_preset_meta(); + meta["default_direction"] = json!("left"); + let err = validate_ai_preset_metadata("ai_preset", &meta).unwrap_err(); + assert!(err.contains("default_direction"), "Feilmelding: {err}"); + } + + #[test] + fn test_validate_ai_preset_invalid_color() { + let mut meta = valid_ai_preset_meta(); + meta["color"] = json!("red"); + let err = validate_ai_preset_metadata("ai_preset", &meta).unwrap_err(); + assert!(err.contains("color"), "Feilmelding: {err}"); + } + #[test] fn test_validate_traits_all_known() { // Verifiser at alle traits fra katalogen er gyldige diff --git a/migrations/015_ai_preset_seeds.sql b/migrations/015_ai_preset_seeds.sql new file mode 100644 index 0000000..7f1e6fe --- /dev/null +++ b/migrations/015_ai_preset_seeds.sql @@ -0,0 +1,168 @@ +-- 015_ai_preset_seeds.sql +-- Oppgave 18.1: AI-preset standardprompter som seed-data. +-- Oppretter 8 gjennomarbeidede AI-presets med node_kind 'ai_preset'. +-- Ref: docs/features/ai_verktoy.md § 3, § 5 + +BEGIN; + +-- ============================================================================= +-- 1. Rens tekst +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000001', + 'ai_preset', + 'Rens tekst', + 'open', + '{ + "prompt": "Du mottar tekst som er kopiert fra en nettside eller et dokument. Teksten inneholder sannsynligvis navigasjonselementer, annonser, knappetekster, og annet støy i tillegg til selve innholdet.\n\nDin oppgave:\n1. Identifiser og behold kun det faktiske innholdet (artikkel, tekst, data).\n2. Fjern all støy: navigasjon, menyer, annonser, knappetekster, copyright-tekst, cookie-varsler.\n3. Behold og fiks formatering: overskrifter, avsnitt, lister, sitater.\n4. Fiks åpenbare skrivefeil og ødelagt formatering.\n5. Oppgi kilde (URL eller publikasjon) hvis det fremgår av teksten.\n\nOutput kun den rensede teksten. Ikke legg til kommentarer om hva du har gjort.", + "model_profile": "flash", + "category": "standard", + "default_direction": "tool_to_node", + "icon": "sparkles", + "color": "#8B5CF6" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 2. Korrektør +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000002', + 'ai_preset', + 'Korrektør', + 'open', + '{ + "prompt": "Du er en grundig korrektør for norsk tekst.\n\nDin oppgave:\n1. Fiks alle skrivefeil og grammatiske feil.\n2. Rett tegnsetting (komma, punktum, bindestrek, anførselstegn).\n3. Sørg for konsistent rettskriving (ikke bland bokmål og nynorsk).\n4. Behold forfatterens stemme, stil og ordvalg — ikke skriv om.\n5. Behold all formatering (overskrifter, lister, avsnitt) uendret.\n\nOutput kun den korrigerte teksten. Ikke legg til kommentarer eller forklaringer.", + "model_profile": "flash", + "category": "standard", + "default_direction": "tool_to_node", + "icon": "check-circle", + "color": "#10B981" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 3. Oppsummering +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000003', + 'ai_preset', + 'Oppsummering', + 'open', + '{ + "prompt": "Oppsummer denne teksten.\n\nRegler:\n1. Kondensr til 2–5 setninger som dekker det viktigste.\n2. Behold nøkkeltall, navn og konkrete påstander.\n3. Skriv i tredjeperson, saklig tone.\n4. Ikke legg til informasjon som ikke finnes i originalen.\n5. Skriv på norsk med mindre originalen er på engelsk.\n\nOutput kun oppsummeringen.", + "model_profile": "flash", + "category": "standard", + "default_direction": "node_to_tool", + "icon": "file-text", + "color": "#3B82F6" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 4. Oversett +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000004', + 'ai_preset', + 'Oversett', + 'open', + '{ + "prompt": "Oversett denne teksten.\n\nRegler:\n1. Hvis teksten er på norsk, oversett til engelsk.\n2. Hvis teksten er på engelsk, oversett til norsk (bokmål).\n3. Hvis teksten er på et annet språk, oversett til norsk (bokmål).\n4. Behold formatering (overskrifter, lister, avsnitt).\n5. Bevar fagtermer der det ikke finnes gode norske/engelske alternativer.\n6. Hold deg tro mot originalen — ikke forenkle eller utvid.\n\nOutput kun oversettelsen.", + "model_profile": "flash", + "category": "standard", + "default_direction": "both", + "icon": "languages", + "color": "#F59E0B" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 5. Skriv om for publisering +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000005', + 'ai_preset', + 'Skriv om for publisering', + 'open', + '{ + "prompt": "Skriv om denne teksten til et publiseringsklart artikkelformat.\n\nStruktur:\n1. Tittel (kort, fengende, informativ)\n2. Ingress (1–2 setninger som oppsummerer essensen)\n3. Brødtekst (ryddig, med avsnitt og eventuelt mellomtitler)\n\nRegler:\n- Behold alle fakta og påstander fra originalen.\n- Skriv i en saklig, profesjonell tone.\n- Bruk aktive setninger og unngå unødvendig fyord.\n- Skriv på norsk med mindre originalen er på engelsk.\n- Ikke legg til informasjon som ikke finnes i kilden.", + "model_profile": "standard", + "category": "standard", + "default_direction": "node_to_tool", + "icon": "pen-line", + "color": "#EC4899" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 6. Trekk ut fakta +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000006', + 'ai_preset', + 'Trekk ut fakta', + 'open', + '{ + "prompt": "Analyser denne teksten og trekk ut alle verifiserbare fakta.\n\nOutput som punktliste:\n- Konkrete påstander og fakta\n- Tall, statistikk og målinger\n- Direkte sitater (med hvem som sa det)\n- Datoer og tidspunkter\n- Navn på personer, organisasjoner, steder\n\nRegler:\n1. Kun fakta som faktisk står i teksten — ikke trekk slutninger.\n2. Hvert punkt skal være selvstendig forståelig.\n3. Marker usikre påstander med [usikkert].\n4. Skriv på norsk med mindre originalen er på engelsk.", + "model_profile": "standard", + "category": "standard", + "default_direction": "node_to_tool", + "icon": "list-checks", + "color": "#6366F1" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 7. Forenkle +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000007', + 'ai_preset', + 'Forenkle', + 'open', + '{ + "prompt": "Skriv om denne teksten i enklere språk.\n\nRegler:\n1. Bruk korte setninger og vanlige ord.\n2. Forklar fagtermer i parentes eller erstatt dem.\n3. Behold all viktig informasjon — ikke kutt innhold.\n4. Behold strukturen (avsnitt, lister, overskrifter).\n5. Målgruppe: en som ikke er ekspert på temaet.\n\nOutput kun den forenklede teksten.", + "model_profile": "flash", + "category": "standard", + "default_direction": "tool_to_node", + "icon": "minimize", + "color": "#14B8A6" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 8. Endre tone +-- ============================================================================= +INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by) +VALUES ( + 'e0000000-a100-4000-a000-000000000008', + 'ai_preset', + 'Endre tone', + 'open', + '{ + "prompt": "Endre tonen i denne teksten.\n\nVelg riktig retning basert på originalen:\n- Hvis teksten er formell → gjør den uformell og engasjert\n- Hvis teksten er uformell → gjør den formell og saklig\n- Hvis teksten er nøytral → gjør den mer engasjert og personlig\n\nRegler:\n1. Behold alt innhold og alle fakta.\n2. Behold strukturen (avsnitt, lister, overskrifter).\n3. Endre kun tone, stil og ordvalg.\n4. Ikke legg til eller fjern informasjon.\n\nOutput kun teksten med endret tone.", + "model_profile": "flash", + "category": "standard", + "default_direction": "tool_to_node", + "icon": "volume-2", + "color": "#F97316" + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +COMMIT; diff --git a/tasks.md b/tasks.md index fd2011c..69fd89e 100644 --- a/tasks.md +++ b/tasks.md @@ -201,8 +201,7 @@ Ref: Kodegjennomgang av `b4c4bb8` (Lydstudio: lydredigering via FFmpeg). Ref: `docs/features/ai_verktoy.md`, `docs/retninger/arbeidsflaten.md` -- [~] 18.1 AI-preset node-type: `node_kind: 'ai_preset'` med metadata (prompt, model_profile, category, icon, color). Maskinrommet validerer ved opprettelse. Seed standardprompter (rens tekst, korrektør, oppsummering, oversett, skriv om, trekk ut fakta, forenkle, endre tone). - > Påbegynt: 2026-03-18T06:05 +- [x] 18.1 AI-preset node-type: `node_kind: 'ai_preset'` med metadata (prompt, model_profile, category, icon, color). Maskinrommet validerer ved opprettelse. Seed standardprompter (rens tekst, korrektør, oppsummering, oversett, skriv om, trekk ut fakta, forenkle, endre tone). - [ ] 18.2 AI-prosessering endepunkt: `POST /intentions/ai_process` med source_node_id, ai_preset_id, direction (node_to_tool / tool_to_node). Maskinrommet henter kilde-content og preset-prompt, mapper modellprofil → LiteLLM-alias, sender til AI Gateway. Logg forbruk i ai_usage_log. - [ ] 18.3 Direction-logikk: `tool_to_node` → lagre original som revisjon, oppdater node content. `node_to_tool` → opprett ny node med AI-output, opprett `derived_from`-edge til kilde + `processed_by`-edge til AI-preset. - [ ] 18.4 AI-verktøy panel (frontend): Svelte-komponent for arbeidsflaten. Prompt-velger med standardprompter, fritekst-felt for egendefinert prompt, modell-indikator (readonly). Drag-and-drop mottak for tekstnoder.