import { Action, ActionPanel, AI, Clipboard, Detail, Form, Icon, showHUD, showToast, Toast, useNavigation, } from "@raycast/api"; import { copyRichText, markdownToHtml, maybeCloseRaycast } from "./ai-views"; import { getSelectedTextSafely } from "./selection"; import { useEffect, useMemo, useRef, useState } from "react"; import { createSession, getActiveSessionId, getSession, listSessions, setActiveSession, updateSessionMapping, } from "./sessions"; import { CREATIVITY_OPTIONS, DEFAULT_MODEL, getLastUsedModel, MODEL_OPTIONS, setLastUsedModel, } from "./ai"; import { buildSummaryPrompt, type Creativity } from "./summarize"; import type { EntityType, VelumSession } from "./types"; import { markdownCodeBlock, mappingDetailTable, NEW_SESSION_ID, sessionSubtitle, } from "./ui"; import { getEntityTypes, localDepseudonymize, normalizePseudonymizeResponse, pseudonymize, } from "./velum"; type FormValues = { text: string; sessionId: string; entityTypes: string[]; model: string; creativity: Creativity; instructions: string; }; export default function Command() { const [text, setText] = useState(""); const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(); const [entityTypes, setEntityTypes] = useState([]); const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); const [model, setModel] = useState(DEFAULT_MODEL); const [isLoading, setIsLoading] = useState(true); const { push } = useNavigation(); useEffect(() => { async function load() { const [loadedSessions, activeId, lastModel] = await Promise.all([ listSessions(), getActiveSessionId(), getLastUsedModel(), ]); setSessions(loadedSessions); setActiveSessionId(activeId); setModel(lastModel); const selection = await getSelectedTextSafely(); if (selection && selection.trim()) { setText(selection); } try { const loadedEntityTypes = await getEntityTypes(); setEntityTypes(loadedEntityTypes); setSelectedEntityTypes(loadedEntityTypes); } catch { setEntityTypes([]); } finally { setIsLoading(false); } } load(); }, []); const defaultSessionId = useMemo( () => activeSessionId || sessions[0]?.id || NEW_SESSION_ID, [activeSessionId, sessions], ); async function loadSelectedText() { const selected = await getSelectedTextSafely(); if (!selected) { await showToast({ style: Toast.Style.Failure, title: "Kein markierter Text gefunden", }); return; } setText(selected); } async function loadClipboardText() { const clipboardText = await Clipboard.readText(); if (!clipboardText) { await showToast({ style: Toast.Style.Failure, title: "Zwischenablage enthält keinen Text", }); return; } setText(clipboardText); } async function handleSubmit(values: FormValues) { const input = values.text.trim(); if (!input) { await showToast({ style: Toast.Style.Failure, title: "Text zum Zusammenfassen eingeben", }); return; } void setLastUsedModel(values.model); const toast = await showToast({ style: Toast.Style.Animated, title: "Pseudonymisiere…", }); try { const session = values.sessionId === NEW_SESSION_ID ? await createSession() : ((await getSession(values.sessionId)) ?? (await createSession())); await setActiveSession(session.id); const rawResult = await pseudonymize( input, session.mapping, values.entityTypes, ); const pseudoResult = normalizePseudonymizeResponse(rawResult); const updatedSession = await updateSessionMapping( session.id, pseudoResult.mapping, pseudoResult.selected_entity_types, ); toast.style = Toast.Style.Success; toast.title = "Pseudonymisiert"; toast.message = `${pseudoResult.entity_count} Zuordnungen`; push( , ); } catch (error) { toast.style = Toast.Style.Failure; toast.title = "Pseudonymisierung fehlgeschlagen"; toast.message = error instanceof Error ? error.message : String(error); } } return (
} > {sessions.map((session) => ( ))} {entityTypes.length > 0 ? ( {entityTypes.map((entityType) => ( ))} ) : null} {MODEL_OPTIONS.map((option) => ( ))} {CREATIVITY_OPTIONS.map((value) => ( ))} ); } type StageProps = { session: VelumSession; pseudonymizedText: string; model: string; creativity: Creativity; instructions: string; }; function ConfirmPseudonymized({ session, pseudonymizedText, model, creativity, instructions, }: StageProps) { const [editedText, setEditedText] = useState(pseudonymizedText); const [editedInstructions, setEditedInstructions] = useState(instructions); const { push } = useNavigation(); async function handleSubmit() { if (!editedText.trim()) { await showToast({ style: Toast.Style.Failure, title: "Pseudonymisierter Text ist leer", }); return; } push( , ); } return (
} > werden nach der Zusammenfassung wiederhergestellt. Sitzung: ${session.name} · ${Object.keys(session.mapping).length} Zuordnungen.`} /> ); } function mappingDescriptionText(session: VelumSession): string { const entries = Object.entries(session.mapping).sort(([a], [b]) => a.localeCompare(b), ); if (entries.length === 0) { return "Keine Einträge — nichts wurde pseudonymisiert."; } return entries .map(([placeholder, entry]) => `${placeholder} → ${entry.original}`) .join("\n"); } type Phase = "summarizing" | "restoring" | "done" | "error"; function SummarizeEmailResult({ session, pseudonymizedText, model, creativity, instructions, }: StageProps) { const [aiBuffer, setAiBuffer] = useState(""); const [restored, setRestored] = useState(null); const [replacementsMade, setReplacementsMade] = useState(0); const [phase, setPhase] = useState("summarizing"); const [error, setError] = useState(null); const controllerRef = useRef(null); useEffect(() => { const controller = new AbortController(); controllerRef.current = controller; let cancelled = false; async function run() { const toast = await showToast({ style: Toast.Style.Animated, title: "Fasse zusammen…", }); try { const prompt = buildSummaryPrompt(pseudonymizedText, instructions); const stream = AI.ask(prompt, { model: model as AI.Model, creativity, signal: controller.signal, }); stream.on("data", (chunk) => { if (cancelled) return; setAiBuffer((prev) => prev + chunk); }); const aiText = await stream; if (cancelled) return; toast.title = "Stelle wieder her…"; setPhase("restoring"); const restoreResult = localDepseudonymize(aiText, session.mapping); if (cancelled) return; setRestored(restoreResult.original_text); setReplacementsMade(restoreResult.replacements_made); setPhase("done"); toast.style = Toast.Style.Success; toast.title = "Fertig"; toast.message = `${restoreResult.replacements_made} Ersetzungen`; } catch (err) { if (cancelled || controller.signal.aborted) return; const message = err instanceof Error ? err.message : String(err); setError(message); setPhase("error"); toast.style = Toast.Style.Failure; toast.title = "Zusammenfassung fehlgeschlagen"; toast.message = message; } } run(); return () => { cancelled = true; controller.abort(); }; }, [pseudonymizedText, model, creativity, instructions, session.mapping]); const phaseLabel = phase === "summarizing" ? "KI generiert Zusammenfassung …" : phase === "restoring" ? "Platzhalter werden ersetzt …" : phase === "error" ? "Fehler" : "Fertig"; const summaryBody = restored ?? aiBuffer; const markdown = [ `# ${session.name}`, sessionSubtitle(session), `*Status:* ${phaseLabel}`, "", summaryBody.trim() ? summaryBody : "_Noch keine Inhalte — warte auf das Modell …_", "", restored ? `*${replacementsMade} Platzhalter ersetzt.*` : "_Originalnamen werden eingesetzt, sobald das Modell fertig ist._", "", "## Pseudonymisierte Eingabe", markdownCodeBlock(pseudonymizedText), "", "## Pseudonymisierte KI-Ausgabe", aiBuffer.trim() ? markdownCodeBlock(aiBuffer) : "_Noch nichts vom Modell empfangen._", "", "## Zuordnung", mappingDetailTable(session.mapping), error ? `\n> Fehler: ${error}` : "", ].join("\n"); const isLoading = phase === "summarizing" || phase === "restoring"; return ( {restored ? ( <> { try { await copyRichText(markdownToHtml(restored), restored); await showHUD("Als Rich Text kopiert"); } catch { await Clipboard.copy(restored); await showHUD("Kopiert (Plain Text Fallback)"); } await maybeCloseRaycast(); }} /> { await Clipboard.copy(restored); await showHUD("Als Markdown kopiert"); await maybeCloseRaycast(); }} /> { await Clipboard.paste({ html: markdownToHtml(restored), text: restored, }); await maybeCloseRaycast(); }} /> ) : null} {aiBuffer ? ( ) : null} } /> ); }