Initial commit: Velum Raycast extension
Pseudonymize-and-AI workflow for handling PII-sensitive text via the Velum API and Raycast AI. Commands cover end-to-end email summary and reply, briefing/action-items/structured-data extraction, manual pseudonymize/depseudonymize on selection or clipboard, and session management. Includes Raycast 2.0 Beta workarounds for selection capture and rich-text clipboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
302
src/extract-action-items.tsx
Normal file
302
src/extract-action-items.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import {
|
||||
Action,
|
||||
ActionPanel,
|
||||
Clipboard,
|
||||
Form,
|
||||
Icon,
|
||||
showToast,
|
||||
Toast,
|
||||
useNavigation,
|
||||
} from "@raycast/api";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
type Creativity,
|
||||
CREATIVITY_OPTIONS,
|
||||
MODEL_OPTIONS,
|
||||
STRICT_PLACEHOLDER_RULE,
|
||||
} from "./ai";
|
||||
import { PseudonymizationConfirm } from "./ai-views";
|
||||
import { getPreferences } from "./preferences";
|
||||
import { getSelectedTextSafely } from "./selection";
|
||||
import {
|
||||
createSession,
|
||||
getActiveSessionId,
|
||||
getSession,
|
||||
listSessions,
|
||||
setActiveSession,
|
||||
updateSessionMapping,
|
||||
} from "./sessions";
|
||||
import type { EntityType, VelumSession } from "./types";
|
||||
import { NEW_SESSION_ID } from "./ui";
|
||||
import {
|
||||
getEntityTypes,
|
||||
normalizePseudonymizeResponse,
|
||||
pseudonymize,
|
||||
} from "./velum";
|
||||
|
||||
function buildActionItemsPrompt(
|
||||
pseudonymizedText: string,
|
||||
instructions: string,
|
||||
): string {
|
||||
const extra = instructions.trim()
|
||||
? `\n\nZusätzliche Anweisungen des Nutzers:\n${instructions.trim()}`
|
||||
: "";
|
||||
|
||||
return [
|
||||
"Du erhältst pseudonymisierten Text (Meeting-Notizen, Email-Thread, Transkript).",
|
||||
"Personenbezogene Daten wurden durch Platzhalter wie <PERSON_1>, <ORG_2>, <DATE_3> ersetzt.",
|
||||
"",
|
||||
STRICT_PLACEHOLDER_RULE,
|
||||
"",
|
||||
"Aufgabe: Extrahiere alle Action Items aus dem Text als deutschsprachige Markdown-Tabelle.",
|
||||
"",
|
||||
"Format:",
|
||||
"| Aufgabe | Verantwortlich | Deadline | Status |",
|
||||
"| --- | --- | --- | --- |",
|
||||
"",
|
||||
"Regeln:",
|
||||
"- Liste nur konkret formulierte Aufgaben/Zusagen, keine vagen Absichten.",
|
||||
'- Verantwortliche werden als Platzhalter referenziert (z. B. <PERSON_1>); wenn unklar, schreibe „—".',
|
||||
'- Deadline: Datum oder Zeitraum aus dem Text. Wenn nichts genannt, „—".',
|
||||
'- Status: „Offen", „In Arbeit" oder „Erledigt" — wenn nicht erkennbar, „Offen".',
|
||||
'- Wenn der Text keine Action Items enthält, antworte mit Kopfzeile, Trennzeile und einer Zeile „| Keine Action Items | — | — | — |".',
|
||||
"",
|
||||
"Antworte ausschließlich mit der Tabelle, ohne einleitende Floskeln und ohne Code-Fences (keine ``` Zeilen).",
|
||||
extra,
|
||||
"",
|
||||
"--- Text (pseudonymisiert) ---",
|
||||
pseudonymizedText,
|
||||
"--- Ende ---",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
type FormValues = {
|
||||
text: string;
|
||||
sessionId: string;
|
||||
entityTypes: string[];
|
||||
model: string;
|
||||
creativity: Creativity;
|
||||
instructions: string;
|
||||
};
|
||||
|
||||
export default function Command() {
|
||||
const preferences = getPreferences();
|
||||
const [text, setText] = useState("");
|
||||
const [sessions, setSessions] = useState<VelumSession[]>([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string>();
|
||||
const [entityTypes, setEntityTypes] = useState<EntityType[]>([]);
|
||||
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { push } = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const [loadedSessions, activeId] = await Promise.all([
|
||||
listSessions(),
|
||||
getActiveSessionId(),
|
||||
]);
|
||||
setSessions(loadedSessions);
|
||||
setActiveSessionId(activeId);
|
||||
|
||||
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 Extrahieren eingeben",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
<PseudonymizationConfirm
|
||||
session={updatedSession}
|
||||
pseudonymizedText={pseudoResult.pseudonymized_text}
|
||||
model={values.model}
|
||||
creativity={values.creativity}
|
||||
instructions={values.instructions}
|
||||
buildPrompt={buildActionItemsPrompt}
|
||||
labels={{
|
||||
pageHeading: "Action Items",
|
||||
runningLabel: "KI extrahiert Action Items …",
|
||||
failureTitle: "Extraktion fehlgeschlagen",
|
||||
copyTitle: "Action Items kopieren",
|
||||
pasteTitle: "Action Items einfügen",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Pseudonymisierung fehlgeschlagen";
|
||||
toast.message = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
isLoading={isLoading}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.SubmitForm
|
||||
title="Action Items extrahieren"
|
||||
icon={Icon.List}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
<Action
|
||||
title="Markierten Text übernehmen"
|
||||
icon={Icon.TextSelection}
|
||||
onAction={loadSelectedText}
|
||||
/>
|
||||
<Action
|
||||
title="Zwischenablage übernehmen"
|
||||
icon={Icon.Clipboard}
|
||||
onAction={loadClipboardText}
|
||||
/>
|
||||
</ActionPanel>
|
||||
}
|
||||
>
|
||||
<Form.TextArea
|
||||
id="text"
|
||||
title="Text"
|
||||
value={text}
|
||||
onChange={setText}
|
||||
placeholder="Meeting-Notizen, Email-Thread, Transkript …"
|
||||
/>
|
||||
<Form.Dropdown
|
||||
id="sessionId"
|
||||
title="Sitzung"
|
||||
defaultValue={defaultSessionId}
|
||||
>
|
||||
<Form.Dropdown.Item
|
||||
value={NEW_SESSION_ID}
|
||||
title="Neue Sitzung"
|
||||
icon={Icon.Plus}
|
||||
/>
|
||||
{sessions.map((session) => (
|
||||
<Form.Dropdown.Item
|
||||
key={session.id}
|
||||
value={session.id}
|
||||
title={session.name}
|
||||
icon={
|
||||
session.id === activeSessionId ? Icon.CheckCircle : Icon.Circle
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Form.Dropdown>
|
||||
{entityTypes.length > 0 ? (
|
||||
<Form.TagPicker
|
||||
id="entityTypes"
|
||||
title="Entitätstypen"
|
||||
value={selectedEntityTypes}
|
||||
onChange={setSelectedEntityTypes}
|
||||
>
|
||||
{entityTypes.map((entityType) => (
|
||||
<Form.TagPicker.Item
|
||||
key={entityType}
|
||||
value={entityType}
|
||||
title={entityType}
|
||||
/>
|
||||
))}
|
||||
</Form.TagPicker>
|
||||
) : null}
|
||||
<Form.Dropdown
|
||||
id="model"
|
||||
title="KI-Modell"
|
||||
defaultValue={preferences.summaryModel}
|
||||
>
|
||||
{MODEL_OPTIONS.map((option) => (
|
||||
<Form.Dropdown.Item
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
title={option.title}
|
||||
/>
|
||||
))}
|
||||
</Form.Dropdown>
|
||||
<Form.Dropdown id="creativity" title="Kreativität" defaultValue="low">
|
||||
{CREATIVITY_OPTIONS.map((value) => (
|
||||
<Form.Dropdown.Item key={value} value={value} title={value} />
|
||||
))}
|
||||
</Form.Dropdown>
|
||||
<Form.TextArea
|
||||
id="instructions"
|
||||
title="Zusätzliche Anweisungen"
|
||||
placeholder="Optional: Fokus auf bestimmte Personen, Zeitraum, …"
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user