feat: complete history, attendees list, and smart templates
This commit is contained in:
169
src/App.tsx
169
src/App.tsx
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user