# Forslag: Universell editor ## Idé Én editor-komponent som brukes overalt i Sidelinja — chat, notater, artikler, kanban-kort, show notes. Editoren autodetekterer format (plaintext, markdown, LaTeX) og rendrer riktig uten at brukeren velger modus. Avansert funksjonalitet er alltid tilgjengelig, aldri påtvunget. ## Kjerneprinsipp: Brukeren bare skriver Editoren forstår hva brukeren skriver og rendrer det riktig — live, uten konfigurasjon: ``` Bruker skriver Autodetektert Rendres som ────────────── ───────────── ─────────── hei plaintext tekst **viktig** markdown bold $E = mc^2$ LaTeX inline formel $$\int_0^1 f(x)dx$$ LaTeX blokk sentrert formel ```python\n... kodeblokk syntax highlight # Overskrift markdown heading H1 #Skolepolitikk mention graf-edge + lenke {{segment:uuid}} podcast-embed lydspiller bilde dratt inn media inline bilde med bildetekst https://youtu.be/xyz YouTube-embed innebygd videospiller https://example.com lenke rik forhåndsvisning (OG-kort) ``` En bruker som kan markdown bruker markdown. En bruker som kan LaTeX bruker LaTeX. En bruker som bare skriver vanlig tekst får vanlig tekst. Ingen modusvelger, ingen forhåndsvalg. ## Hvorfor er dette et eget prosjekt? Editoren er det mest komplekse enkeltkomponenten i Sidelinja: - Autodeteksjon av formater (markdown, LaTeX, mentions, embeds) - Progressiv toolbar (fra usynlig til fullverdig) - Live rendering av alt innhold - `#`-mention med autocomplete og graf-integrasjon - Podcast-embeds, bilder, vedlegg - Versjonering og auto-save - Format-kontekstsensitivitet (emoji i chat vs sirlig typografi i artikler) - Fremtidig: collaborative editing (Yjs) - Mobilopplevelse Alt dette fortjener sin egen spec, sin egen utviklingssyklus, og sin egen iterasjon — uavhengig av tekst-primitiven (arkitektur) og artikkel-publisering (distribusjon). ## Hva bygger den på? - **Tekst-primitiv** — filosofien om at enhver melding kan vokse - **Meldingsboks** — datamodellen editoren skriver til (`messages.body`) - **Kunnskapsgraf** — `#`-mentions oppretter graf-edges - **Message revisions** — editoren trigge lagring av revisjoner ## Skisse ### Teknologivalg: Tiptap (ProseMirror) Tiptap er det naturlige valget for SvelteKit: - ProseMirror-basert, modular, godt vedlikeholdt - Headless — full kontroll over UI og toolbar - Lagrer som JSON (strukturert, transformerbart) - Utvidbar med custom nodes og marks - Svelte-kompatibelt (`@tiptap/core` headless + egen Svelte-wrapper) - Collaborative editing via Yjs-plugin (fase 2) ### Progressiv toolbar Editoren er **én komponent** (``) med ulik toolbar-konfigurasjon basert på kontekst: **Kompakt** (default i chat, kanban-kort, quick reply): ``` [tekst-input .......................... ↑] [Send] ``` Ingen synlig toolbar. `#`-mentions og inline-formatering (bold, italic, lenker) via keyboard shortcuts. Enter = send. `↑`-knappen utvider til full modus. **Utvidet** (notater, lengre tekster, "↑" fra kompakt): ``` [Tittel ] [B I S ~ | H1 H2 H3 | • — ✓ | 🔗 📎 # | ↓ ] [ ] [ editor-innhold med live-rendering ] [ ] ``` Full toolbar. Enter = ny linje. Auto-save. `↓` kollapser tilbake. **Publisering** (artikler med `article_view`): ``` [Tittel ] [B I S ~ | H1 H2 H3 | • — ✓ | 🔗 📎 # | ƒ 🎙️ |📝] [ ] [ editor-innhold med sidemerknad-støtte ] [ ] [Slug: ________] [Status: Utkast ▼] [Forhåndsvis] [Publiser] ``` Alt fra utvidet + LaTeX-toolbar, podcast-embeds, fotnoter, publiseringskontroller. `📝`-knappen åpner sidemerknad-panel. **Modusene er ikke låst.** Brukeren kan alltid utvide eller kollapse. Toolbar-nivå er en UI-preferanse per kontekst, ikke en datadistinksjon. ### Raw / Rendered — brukeren bestemmer Editoren har to visninger, alltid tilgjengelig via en enkel toggle (Ctrl+/ eller knapp i toolbar): **Raw:** Viser kildekoden. Markdown som markdown, LaTeX som LaTeX, mentions som `#Skolepolitikk`. En monospace-editor der du ser nøyaktig hva som er lagret. Ingen magi, ingen overraskelser. Syntax highlighting for lesbarhet, men ingen transformasjon av det du skriver. ``` # Min analyse av skolepolitikken Gini-koeffisienten $G = \frac{1}{2n^2\bar{x}}$ viser at... **Viktig:** Se også #Utdanningsforbundet sin rapport. {{segment:550e8400-e29b-41d4-a716-446655440000}} ``` **Rendered:** WYSIWYG-visning. Overskrifter er store, bold er bold, LaTeX er rendret som formler, mentions er klikkbare lenker, podcast-embeds er lydspillere. Du kan skrive direkte i denne visningen — toolbar-knapper og formatering fungerer som i et vanlig tekstbehandlingsverktøy. ``` ┌──────────────────────────────────────────────────┐ │ Min analyse av skolepolitikken [Raw/Rendered] │ │ │ Gini-koeffisienten G = ½n²x̄ viser at... │ │ │ │ Viktig: Se også Utdanningsforbundet sin rapport. │ │ │ │ ▶ [Episode 42, 14:23–21:07] ░░░░░░░░ │ └──────────────────────────────────────────────────┘ ``` **Prinsippet:** Rendered er standard for de fleste. Raw er alltid ett tastetrykk unna for de som vil ha kontroll. Begge visningene redigerer samme data — switch er øyeblikkelig og tapsfri. **Teknisk:** Raw-modus er en CodeMirror-instans (eller enkel textarea med syntax highlighting) som opererer på serialisert markdown/LaTeX. Rendered-modus er Tiptap WYSIWYG. Begge skriver til samme Tiptap JSON — raw via en markdown→JSON parser, rendered direkte. Switch mellom dem re-serialiserer. **Viktig detalj:** I rendered-modus forsvinner ikke kildekoden. Klikker du på en rendret formel, ser du LaTeX-kilden inline (som Obsidian gjør med markdown). Klikker du utenfor, rendres den igjen. Rendered-modus er altså ikke et "preview" — det er en visuell editor der kildekoden er tilgjengelig ved klikk. ### Autodeteksjon Editoren forstår hva brukeren skriver — i begge moduser: **Markdown:** `**bold**`, `# Heading`, `- item`, `> quote`, `` `code` ``. I raw: syntax highlighted. I rendered: rendret som formatering. **LaTeX:** `$...$` inline, `$$...$$` blokk. I raw: syntax highlighted. I rendered: rendret med KaTeX. **Mentions:** `#` trigger autocomplete i begge moduser. Ved valg opprettes en mention-node som rendres som lenke (rendered) eller `#Navn` (raw), og oppretter `MENTIONS`-edge ved lagring. **Podcast-embeds:** `{{segment:uuid}}`. I raw: syntax highlighted. I rendered: mini-lydspiller. **Kodeblokker:** Triple backtick med språk-hint. Syntax highlighting i begge moduser. ### Media, lenker og embeds Editoren håndterer bilder, lenker og eksterne embeds som førsteklasses innhold — ikke som vedlegg på siden, men inline i teksten. **Bilder:** - Drag-and-drop, paste fra utklippstavle, eller opplastingsknapp i toolbar - Lastes opp til `media_files` (eksisterende tabell) via API - Inline i teksten med valgfri bildetekst og alt-tekst - Resize ved å dra i hjørnet (rendered-modus) - I raw: `![alt-tekst](media:uuid)` eller standard markdown bildesyntaks - Responsiv `srcset` genereres ved opplasting (jobbkø) for publiserte artikler **Lenker:** - Paste en URL → autodetekteres som lenke - Rik forhåndsvisning (Open Graph): tittel, beskrivelse, miniatyrbilde — hentes asynkront ved paste, caches - Brukeren kan velge mellom rik forhåndsvisning (kort) og enkel tekstlenke - I raw: standard markdown `[tekst](url)` eller bare URL **Externe embeds:** - YouTube, Vimeo, Twitter/X, o.l. — paste en URL, editoren gjenkjenner domenet og rendrer innebygd spiller/visning - Basert på oEmbed-protokollen der tilgjengelig, ellers sandboxed iframe - I raw: bare URL-en på egen linje. I rendered: innebygd player med riktig aspekt-ratio - Whitelist-basert: kun kjente domener får embed-behandling. Ukjente URL-er vises som rik lenke-forhåndsvisning **Filvedlegg:** - PDF, dokumenter, andre filer — lastes opp til `media_files`, vises som nedlastbar lenke med filtype-ikon og størrelse **Teknisk:** - Alle opplastinger går via `POST /api/media` → lagres content-addressable i `media_files` - Referanser i Tiptap JSON er `media:uuid` — aldri direkte fil-URL-er. Gjør det mulig å flytte lagring (lokal → CDN) uten å endre innhold - Bildeoptimalisering (resize, WebP-konvertering) som jobbkø-oppgave ved opplasting - oEmbed/OG-metadata caches i en enkel tabell for å unngå gjentatte oppslag ### AI-behandling — universell knapp Editoren har en AI-knapp (✨) som behandler innholdet i boksen. Originalteksten bevares alltid som revisjon (`message_revisions`), og AI-resultatet tar over som nytt innhold — klart for videre redigering av brukeren. Det som opprinnelig var tenkt som en separat "AI Research-Klipper"-modal er nå bare én av handlingene her: paste inn hva som helst → trykk ✨ → AI-en behandler det. **Flyten:** ``` Bruker limer inn rotete Ctrl+A-tekst fra en nettavis → Trykker ✨ (standard: "Fiks tekst") → Originalen lagres som revisjon (alltid tilgjengelig) → AI-resultatet erstatter innholdet i editoren → Brukeren redigerer videre, legger til tittel, justerer → AI-en foreslår #-mentions basert på innholdet → graf-edges opprettes → Meldingen lever videre: kan få prominens i avisen, bli et kanban-kort, publiseres ``` **Alternativt:** Brukeren kan velge at resultatet publiseres som *ny melding* (svar på originalen) i stedet for å erstatte innholdet. Nyttig for "trekk ut fakta" der originalen og resultatet er to ulike ting. ### Standard-prompt: "Fiks tekst" (✨) Standardhandlingen — den brukeren får ved å trykke ✨ uten å åpne menyen — er en generisk "magi"-prompt: ``` Fiks denne teksten. Output på norsk. - Fiks skrivefeil og grammatikk - Start med en kort oppsummering av det viktigste (2–3 setninger) - Fjern metainformasjon, navigasjon, annonser og annen støy fra innlimt webinnhold - Dersom det er tydelig hva kilden er, oppgi den etter innledende oppsummering - Behold saklig innhold og fakta intakt - Bruk markdown-formatering der det gir bedre lesbarhet ``` Denne prompten kan justeres per workspace i Prompt Lab (se `docs/features/prompt_lab.md`). Men standarden skal være god nok til at brukeren bare kan lime inn og trykke ✨ uten å tenke. ### Handlingsmeny (lang-trykk eller ▼ ved siden av ✨) For mer spesifikke behov åpnes en meny: | Handling | Hva AI-en gjør | |---|---| | ✨ Fiks tekst (standard) | Rens, oppsummer, fiks feil, identifiser kilde | | Trekk ut fakta | Identifiserer påstander, tall, sitater — som separate faktoider (ny melding) | | Skriv om for publisering | Omskriver til artikkelformat med tittel, ingress, struktur | | Oversett | Oversetter til valgt språk | | Custom | Brukerens egne prompts fra Prompt Lab | ### Revisjon og sporbarhet Når ✨ brukes: 1. Nåværende innhold lagres i `message_revisions` (originalen er alltid tilgjengelig) 2. AI-resultatet erstatter `messages.body` 3. `messages.metadata` oppdateres med `{ ai_processed: true, ai_action: 'fix_text', ai_prompt_id: '...' }` 4. Brukeren ser resultatet i editoren og kan redigere videre, angre (gå tilbake til revisjon), eller kjøre ✨ igjen Revisjonshistorikken viser tydelig hva som var original og hva som er AI-behandlet. Brukeren kan alltid gå tilbake. ### Teknisk - `POST /api/ai/process` med `{ message_id, action, prompt_id? }` - Oppretter jobbkø-oppgave (`ai_text_process`) - Rust-worker sender til AI Gateway (`http://ai-gateway:4000/v1`) - Ved "erstatt innhold": lagre revisjon + oppdater `messages.body` - Ved "ny melding": opprett ny melding med `reply_to = original_message_id` - AI-foreslåtte `#`-mentions vises for bruker-godkjenning før edges opprettes ### Kontekst-bevisst (Agentic RAG) AI-en kjenner konteksten meldingen lever i. Hvis den er i en channel knyttet til #Skolepolitikk, brukes det som hint for å identifisere relevante entiteter og fakta. Workspace-kontekst + graf-nabolag gir bedre resultater enn en kontekstløs prompt. **RAG-berikelse via kunnskapsgrafen:** Når brukeren skriver et utkast og nevner `#Skolepolitikk`, gjør SvelteKit et usynlig vektorsøk (pgvector) i bakgrunnen mot kunnskapsgrafen *før* prompten sendes til AI Gateway. System-prompten oppdateres dynamisk: ``` Sidelinja har tidligere etablert disse faktaene om Skolepolitikk: - [Faktoide 1 fra grafen] - [Faktoide 2 fra grafen] - [Relatert segment fra Episode 42] Ta hensyn til dette i behandlingen. ``` Resultatet: AI-en ikke bare retter skrivefeil, men fyller inn kontekst spesifikk for redaksjonens kunnskapsbase. Krever pgvector-migrasjon (0006) og `generate_embeddings`-jobbtype. ### Format-kontekst Ikke alt passer overalt. En emoji-rik chatmelding og en sirlig publisert artikkel har ulike estetiske forventninger: **Chat-kontekst:** Alt tillatt. Emojis, GIFs, korte meldinger, uformelt. **Publiserings-kontekst:** Editoren tilbyr et "publiseringsfilter" — en valgfri siste-sjekk som flagger potensielle stilbrudd (emojis i overskrifter, manglende alt-tekst på bilder, etc.). Aldri blokkerende — bare forslag. Publiseringskonteksten tilbyr en "forhåndsvisning som leser" der teksten rendres i den ferdige typografi-stacken (Literata, marginer, sidemerknad-fotnoter) slik at forfatteren ser hvordan det blir. Dette er en tredje visning — read-only, ren leseopplevelse — tilgjengelig via [Forhåndsvis]-knappen i publiserings-modus. ### Brukerinnstillinger Skriftstørrelse, linjehøyde, font, tema og andre visuelle preferanser styres per bruker. Se `docs/features/brukerinnstillinger.md` for full spec — inkludert datamodell (`users.settings` JSONB), CSS custom properties, innstillingspanel og editor-spesifikke preferanser (standard Raw/Rendered, stavekontroll, tegnteller). ### Lagringsformat **Tiptap JSON** som universelt format: ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Gini-koeffisienten: " }, { "type": "math_inline", "attrs": { "latex": "G = \\frac{1}{2n^2\\bar{x}}" } } ]}, { "type": "mention", "attrs": { "id": "uuid", "label": "Skolepolitikk" } } ] } ``` En enkel "hei" og en 3000-ords artikkel med LaTeX bruker samme format. **Body-strategi:** ``` messages.body → Tiptap JSON (universelt kildeformat) messages.metadata → { body_html: '...', body_format: 'tiptap' } ``` **Bakoverkompatibilitet:** Eksisterende ren-tekst-meldinger (der `body` ikke er gyldig JSON) tolkes som plaintext. Editoren wrapper dem i Tiptap-paragraph ved redigering. Ingen migrering — bare fallback i lesekoden. **Pre-rendret HTML:** `body_html` beregnes ved lagring. Brukes for: - Rask visning i feeds og lister (ingen klient-parsing) - Publiserte artikler (KaTeX ferdig-rendret, ingen JS for lesere) - RSS/Atom-feeds - Søkeindeksering ### Auto-save og versjonering **Auto-save:** 500ms debounce etter siste tastetrykk (identisk med dagens notat-mønster). Visuell feedback: "Lagrer..." → "Lagret [tidspunkt]". **Versjonshistorikk:** `message_revisions` lagrer `body` ved hver lagring (eller ved signifikant endring — delta-basert for å unngå å lagre hvert tastetrykk). Brukeren kan bla gjennom tidligere versjoner og tilbakestille. **Fremtidig:** Navngitte snapshots ("Kladd 1", "Sendt til review", etc.) via `metadata` på revisjonen. Ikke dag 1. ### Keyboard shortcuts Konsistente overalt: ``` Ctrl+B → bold Ctrl+I → italic Ctrl+K → lenke Ctrl+Shift+M → math (LaTeX-blokk) # → mention-autocomplete Tab/Shift+Tab → innrykk i lister ``` **Enter-oppførsel:** - Kompakt modus: Enter = send. Shift+Enter = linjeskift. - Utvidet/publisering: Enter = ny linje. Ctrl+Enter = eksplisitt lagre (auto-save gjør det uansett). **Toggle:** - Ctrl+/ → switch mellom Raw og Rendered. ### Lazy loading av extensions Kompakt modus laster bare: - Plaintext - Mentions (`#`-autocomplete) - Inline formatting (bold, italic, lenke) Ved Expand lastes: - Overskrifter, lister, blokk-quotes - Bilder, vedlegg - Kodeblokker med syntax highlighting Ved publiserings-modus lastes: - KaTeX (LaTeX-rendering) - Podcast-embeds - Sidemerknad-fotnoter God for bundle-størrelse og oppstartstid. ## Åpne spørsmål ### Tekniske - **Tiptap JSON vs Markdown som kildeformat?** JSON er editor-vennlig. Markdown er portabelt. Anbefaling: JSON som primær, Markdown-import/-eksport som transformasjon. - **Ytelse:** Tiptap JSON for millioner av chatmeldinger? ~60 bytes overhead per melding. Trolig neglisjerbart, men verdt å måle. - **KaTeX i editoren:** Live-rendering av LaTeX krever KaTeX lastet i editoren. ~300KB gzipped. Akseptabelt for utvidet/publisering, for mye for kompakt? Lazy load løser det. - **Collaborative editing:** Tiptap + Yjs er veletablert. Ikke dag 1. Auto-save + `message_revisions` + optimistic locking (`updated_at`) er nok initialt. ### UX - **Overgangen kompakt → utvidet:** Hvordan føles det? Smooth animasjon? Teksten forblir, toolbar glir inn? Eller instant switch? - **Autodeteksjon av LaTeX i kompakt modus:** Rendres `$E=mc^2$` i en kort chatmelding? Ja — rendring er universell, toolbar er kontekstbetinget. - **Mobile:** Toolbar på liten skjerm? Trolig: floating toolbar som dukker opp ved tekstseleksjon (Medium-stil) i stedet for fast toolbar. - **Paste fra eksterne kilder:** Paste av HTML (fra nettside), Markdown (fra Obsidian), ren tekst? Tiptap håndterer HTML-paste. Markdown-paste krever custom paste handler. ### Format-kontekst - **Emoji-filtrering i publisering:** For rigid? Brukeren bør ha full frihet. Kanskje bare en visuell advarsel i forhåndsvisning, aldri blokkering. - **Ulike typografi-profiler?** En personlig blogg kan ha annen estetikk enn et magasin. Tema per publikasjon (se artikkel-publisering)? ## Innsats: Middels–Stor Tiptap-integrasjon er rett frem. Autodeteksjon, progressiv toolbar, mentions, LaTeX, podcast-embeds, auto-save, versjonering, mobilopplevelse — summen er betydelig. Bør bygges inkrementelt: 1. **Fase 1:** Tiptap med plaintext + mentions + markdown formatting. Kompakt og utvidet modus. Auto-save. 2. **Fase 2:** LaTeX (KaTeX), kodeblokker, bilder/vedlegg. Publiserings-modus. 3. **Fase 3:** Podcast-embeds, sidemerknad-fotnoter, collaborative editing (Yjs). ## Wow-faktor: Høy En editor der du bare skriver — markdown rendres automatisk, LaTeX rendres automatisk, mentions oppretter graf-koblinger, og alt kan vokse til en publisert artikkel — er en opplevelse de fleste verktøy ikke tilbyr. Det nærmeste er Notion, men uten graf-integrasjon og uten podcast-embeds. ## Relasjon til andre proposals og features - **Tekst-primitiv** — filosofien editoren realiserer - **Artikkel-publisering** — publiseringslaget som bruker editoren - **Personlig workspace** — konteksten der editoren brukes daglig - **Meldingsboks** (feature) — datamodellen editoren skriver til - **Komponerbare sider** — maximize gir editoren plass til å gå fra chatboks til fullskjerm skriveverksted - **Chat** (feature) — kompakt modus erstatter dagens chat-input - **Notater** (feature) — utvidet modus erstatter dagens textarea