feat: complete history, attendees list, and smart templates

This commit is contained in:
michael.borak
2026-01-20 15:00:56 +01:00
parent d266de942a
commit 52ccd7ee03
18 changed files with 2222 additions and 480 deletions

View File

@@ -6,18 +6,37 @@ import Recorder from "./components/Recorder";
import LogViewer, { LogEntry } from "./components/LogViewer";
import TranscriptionView from "./components/TranscriptionView";
import Tabs from "./components/Tabs";
import MeetingsView from "./components/MeetingsView";
import HistoryView from "./components/HistoryView";
import ToastContainer, { ToastMessage, ToastType } from "./components/ui/Toast";
export interface PromptTemplate {
id: string;
name: string;
content: string;
keywords?: string[];
}
function App() {
const [view, setView] = useState<'recorder' | 'logs' | 'settings' | 'transcription'>('recorder');
// Keep track of the *previous* tab to return to from settings
const [lastTab, setLastTab] = useState<'recorder' | 'logs' | 'transcription'>('recorder');
const [view, setView] = useState<'recorder' | 'logs' | 'settings' | 'transcription' | 'meetings' | 'history'>('recorder');
const [lastTab, setLastTab] = useState<'recorder' | 'logs' | 'transcription' | 'meetings' | 'history'>('recorder');
// Auto-start recording state to handle "Join & Record" transition
const [autoStartRecording, setAutoStartRecording] = useState(false);
const [recordingSubject, setRecordingSubject] = useState<string>('');
// Toast State
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const addToast = (message: string, type: ToastType = 'info', duration = 3000) => {
const id = Date.now().toString() + Math.random().toString();
setToasts(prev => [...prev, { id, message, type, duration }]);
};
const removeToast = (id: string) => {
setToasts(prev => prev.filter(t => t.id !== id));
};
const [apiKey, setApiKey] = useState(localStorage.getItem('infomaniak_api_key') || '');
const [productId, setProductId] = useState(localStorage.getItem('infomaniak_product_id') || '');
const [savePath, setSavePath] = useState(localStorage.getItem('infomaniak_save_path') || '');
@@ -61,7 +80,8 @@ Kurze Stichpunkte zu Themen, die besprochen, aber noch nicht final geklärt wurd
| [Aufgabe 2] | [Name] | [Datum] |
## 5. Nächste Schritte / Nächstes Meeting
Kurze Info zum weiteren Vorgehen.`
Kurze Info zum weiteren Vorgehen.`,
keywords: ['protokoll', 'meeting', 'team', 'daily', 'weekly']
},
{
id: '2',
@@ -96,7 +116,8 @@ Thema B: [Kurze Zusammenfassung]
| Wer? | Was ist zu tun / zu beachten? | Bis wann? |
| :--- | :--- | :--- |
| [Name] | [Aufgabe] | [Datum] |
| [Name] | [Aufgabe] | [Datum] |`
| [Name] | [Aufgabe] | [Datum] |`,
keywords: ['personal', 'privat', 'vertraulich', 'entwicklungsgespräch', 'feedback', 'unter vier augen']
},
{
id: '3',
@@ -138,7 +159,8 @@ Teilnehmer: [Namen Kunden] & [Namen Intern]
[ ] [Aufgabe, z.B. Zugangdaten senden, Design freigeben] (bis [Datum])
## 5. Nächster Termin / Timeline
Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`
Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`,
keywords: ['beratung', 'kunde', 'client', 'angebot', 'projekt', 'extern']
}
];
@@ -168,6 +190,8 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`
date: string;
transcription: string;
summary: string;
subject?: string;
filename?: string;
}
const [history, setHistory] = useState<HistoryItem[]>(() => {
@@ -179,16 +203,39 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`
const transToSave = t !== undefined ? t : transcription;
const sumToSave = s !== undefined ? s : summary;
// Sanitize subject for filename
const safeSubject = recordingSubject
? recordingSubject.replace(/[^a-zA-Z0-9_-]/g, '_')
: `Meeting_${Date.now()}`;
const filename = `${safeSubject}.md`;
if (!transToSave && !sumToSave) return;
const newItem: HistoryItem = {
id: Date.now().toString(),
date: new Date().toLocaleString(),
transcription: transToSave,
summary: sumToSave
summary: sumToSave,
subject: recordingSubject || "Untitled Recording",
filename: filename
};
const newHistory = [newItem, ...history];
setHistory(newHistory);
localStorage.setItem('infomaniak_history', JSON.stringify(newHistory));
// Persist to Disk (Markdown)
const content = `# ${newItem.subject}\nDate: ${newItem.date}\n\n## Summary\n${sumToSave}\n\n## Transcription\n${transToSave}`;
// If savePath is set, we use it. If not, backend defaults to temp. Here we want to save text.
// Let's assume savePath is set or we default to Documents/Hearbit (if we could).
// For now, if savePath is set, use it.
if (savePath) {
// We need invoke to save text
import("@tauri-apps/api/core").then(({ invoke }) => {
invoke('save_text_file', { path: `${savePath}/${filename}`, content })
.then(() => addToast('Transcript saved to file', 'success'))
.catch(e => addToast(`Failed to save file: ${e}`, 'error'));
});
}
};
const handleDeleteHistory = (id: string) => {
@@ -200,7 +247,7 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`
const handleLoadHistory = (item: HistoryItem) => {
setTranscription(item.transcription);
setSummary(item.summary);
setView('recorder'); // Ensure we go back to recorder to see it
setView('transcription'); // Switch to Transcription view to see content
};
// Logs State
@@ -224,7 +271,7 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`
<div className="absolute right-4 top-4">
<button
onClick={() => {
setLastTab(view === 'logs' ? 'logs' : 'recorder');
setLastTab(view === 'logs' || view === 'history' ? view : 'recorder');
setView('settings');
}}
className="p-2 text-muted-foreground hover:text-foreground hover:bg-secondary rounded-full transition-colors"
@@ -234,53 +281,79 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`
</div>
<Tabs
currentTab={view as 'recorder' | 'logs' | 'transcription'}
currentTab={view as 'recorder' | 'logs' | 'transcription' | 'meetings' | 'history'}
onTabChange={(t) => setView(t)}
/>
</div>
)}
<div className="flex-1 flex flex-col h-full overflow-hidden relative">
{view === 'recorder' && (
<Recorder
apiKey={apiKey}
productId={productId}
prompts={prompts}
onOpenSettings={() => {
setLastTab('recorder');
setView('settings');
}}
transcription={transcription}
setTranscription={setTranscription}
summary={summary}
setSummary={setSummary}
history={history}
onSaveToHistory={handleSaveToHistory}
onDeleteHistory={handleDeleteHistory}
onLoadHistory={handleLoadHistory}
savePath={savePath}
onRecordingComplete={() => setView('transcription')}
/>
)}
<div className="flex-1 flex h-full overflow-hidden relative">
<div className="flex-1 flex flex-col h-full overflow-hidden relative">
{view === 'recorder' && (
<Recorder
apiKey={apiKey}
productId={productId}
prompts={prompts}
onOpenSettings={() => {
setLastTab('recorder');
setView('settings');
}}
transcription={transcription}
setTranscription={setTranscription}
summary={summary}
setSummary={setSummary}
history={history}
onSaveToHistory={handleSaveToHistory}
onDeleteHistory={handleDeleteHistory}
onLoadHistory={handleLoadHistory}
savePath={savePath}
{view === 'transcription' && (
<TranscriptionView transcription={transcription} summary={summary} />
)}
onRecordingComplete={() => setView('transcription')}
autoStart={autoStartRecording}
recordingSubject={recordingSubject}
onAutoStartHandled={() => setAutoStartRecording(false)}
addToast={addToast}
/>
)}
{view === 'logs' && (
<LogViewer logs={logs} />
)}
{view === 'transcription' && (
<TranscriptionView transcription={transcription} summary={summary} />
)}
{view === 'settings' && (
<Settings
onSave={handleSaveSettings}
onClose={() => setView(lastTab)}
apiKey={apiKey}
productId={productId}
prompts={prompts}
savePath={savePath}
/>
)}
{view === 'history' && (
<HistoryView
history={history}
onLoad={handleLoadHistory}
onDelete={handleDeleteHistory}
/>
)}
{view === 'meetings' && (
<MeetingsView
onStartRecording={(subject) => {
setView('recorder');
setRecordingSubject(subject || '');
setAutoStartRecording(true);
}}
/>
)}
{view === 'logs' && (
<LogViewer logs={logs} />
)}
{view === 'settings' && (
<Settings
onSave={handleSaveSettings}
onClose={() => setView(lastTab)}
apiKey={apiKey}
productId={productId}
prompts={prompts}
savePath={savePath}
/>
)}
</div>
<ToastContainer toasts={toasts} removeToast={removeToast} />
</div>
</div>
);