summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/components/files/FileDetail.tsx143
-rw-r--r--makima/frontend/src/components/files/FileList.tsx96
-rw-r--r--makima/frontend/src/components/listen/ControlPanel.tsx11
4 files changed, 247 insertions, 4 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 875af5a..4e90d4d 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -9,6 +9,7 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
+ { label: "Files", href: "/files" },
{ label: "Mesh", href: "/mesh", disabled: true },
{ label: "Register", href: "/register", disabled: true },
{ label: "Login", href: "/login", disabled: true },
diff --git a/makima/frontend/src/components/files/FileDetail.tsx b/makima/frontend/src/components/files/FileDetail.tsx
new file mode 100644
index 0000000..643f35e
--- /dev/null
+++ b/makima/frontend/src/components/files/FileDetail.tsx
@@ -0,0 +1,143 @@
+import { useState } from "react";
+import type { FileDetail as FileDetailType } from "../../lib/api";
+
+interface FileDetailProps {
+ file: FileDetailType;
+ loading: boolean;
+ onBack: () => void;
+ onSave: (id: string, name: string, description: string) => void;
+ onDelete: (id: string) => void;
+}
+
+export function FileDetail({
+ file,
+ loading,
+ onBack,
+ onSave,
+ onDelete,
+}: FileDetailProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [name, setName] = useState(file.name);
+ const [description, setDescription] = useState(file.description || "");
+
+ const handleSave = () => {
+ onSave(file.id, name, description);
+ setIsEditing(false);
+ };
+
+ const handleCancel = () => {
+ setName(file.name);
+ setDescription(file.description || "");
+ setIsEditing(false);
+ };
+
+ if (loading) {
+ return (
+ <div className="panel h-full flex items-center justify-center">
+ <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="panel h-full flex flex-col">
+ {/* Header */}
+ <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
+ <div className="flex items-center justify-between mb-3">
+ <button
+ onClick={onBack}
+ className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
+ >
+ &larr; Back to list
+ </button>
+ <div className="flex gap-2">
+ {isEditing ? (
+ <>
+ <button
+ onClick={handleCancel}
+ className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleSave}
+ className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
+ >
+ Save
+ </button>
+ </>
+ ) : (
+ <>
+ <button
+ onClick={() => setIsEditing(true)}
+ className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors uppercase"
+ >
+ Edit
+ </button>
+ <button
+ onClick={() => onDelete(file.id)}
+ className="px-3 py-1.5 font-mono text-xs text-red-400 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
+ >
+ Delete
+ </button>
+ </>
+ )}
+ </div>
+ </div>
+
+ {isEditing ? (
+ <div className="space-y-3">
+ <input
+ type="text"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
+ placeholder="File name"
+ />
+ <textarea
+ value={description}
+ onChange={(e) => setDescription(e.target.value)}
+ className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
+ rows={2}
+ placeholder="Description (optional)"
+ />
+ </div>
+ ) : (
+ <>
+ <h2 className="font-mono text-lg text-[#dbe7ff] mb-1">
+ {file.name}
+ </h2>
+ {file.description && (
+ <p className="font-mono text-sm text-[#9bc3ff]">
+ {file.description}
+ </p>
+ )}
+ </>
+ )}
+ </div>
+
+ {/* Transcript */}
+ <div className="flex-1 overflow-y-auto p-4 space-y-3">
+ {file.transcript.length === 0 ? (
+ <div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8">
+ No transcript entries.
+ </div>
+ ) : (
+ file.transcript.map((entry) => (
+ <div key={entry.id} className="font-mono text-sm">
+ <div className="flex items-baseline gap-2 mb-1">
+ <span className="text-[#75aafc] text-xs">
+ [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s]
+ </span>
+ <span className="text-[#9bc3ff] text-xs font-bold">
+ {entry.speaker}
+ </span>
+ </div>
+ <p className="m-0 text-[#dbe7ff] leading-relaxed">{entry.text}</p>
+ </div>
+ ))
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/files/FileList.tsx b/makima/frontend/src/components/files/FileList.tsx
new file mode 100644
index 0000000..7e1eea4
--- /dev/null
+++ b/makima/frontend/src/components/files/FileList.tsx
@@ -0,0 +1,96 @@
+import type { FileSummary } from "../../lib/api";
+
+interface FileListProps {
+ files: FileSummary[];
+ loading: boolean;
+ onSelect: (id: string) => void;
+ onDelete: (id: string) => void;
+}
+
+function formatDuration(seconds: number | null): string {
+ if (seconds === null) return "-";
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+}
+
+function formatDate(dateStr: string): string {
+ const date = new Date(dateStr);
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+export function FileList({
+ files,
+ loading,
+ onSelect,
+ onDelete,
+}: FileListProps) {
+ if (loading) {
+ return (
+ <div className="panel h-full flex items-center justify-center">
+ <div className="font-mono text-[#9bc3ff] text-sm">Loading files...</div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="panel h-full flex flex-col">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase p-4 pb-2 border-b border-dashed border-[rgba(117,170,252,0.35)]">
+ FILES//
+ </div>
+
+ <div className="flex-1 overflow-y-auto">
+ {files.length === 0 ? (
+ <div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8">
+ No saved files yet. Start recording to create one.
+ </div>
+ ) : (
+ <div className="divide-y divide-[rgba(117,170,252,0.15)]">
+ {files.map((file) => (
+ <div
+ key={file.id}
+ className="p-4 hover:bg-[rgba(117,170,252,0.05)] transition-colors"
+ >
+ <div className="flex items-start justify-between gap-4">
+ <button
+ onClick={() => onSelect(file.id)}
+ className="flex-1 text-left"
+ >
+ <h3 className="font-mono text-sm text-[#dbe7ff] mb-1">
+ {file.name}
+ </h3>
+ {file.description && (
+ <p className="font-mono text-xs text-[#9bc3ff] mb-2 line-clamp-2">
+ {file.description}
+ </p>
+ )}
+ <div className="flex gap-4 font-mono text-[10px] text-[#75aafc]">
+ <span>{file.transcriptCount} segments</span>
+ <span>{formatDuration(file.duration)}</span>
+ <span>{formatDate(file.createdAt)}</span>
+ </div>
+ </button>
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ onDelete(file.id);
+ }}
+ className="px-2 py-1 font-mono text-[10px] text-red-400 hover:bg-red-400/10 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
+ >
+ Delete
+ </button>
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/listen/ControlPanel.tsx b/makima/frontend/src/components/listen/ControlPanel.tsx
index 25dbefe..af2cd05 100644
--- a/makima/frontend/src/components/listen/ControlPanel.tsx
+++ b/makima/frontend/src/components/listen/ControlPanel.tsx
@@ -6,8 +6,9 @@ interface ControlPanelProps {
isConnected: boolean;
micStatus: MicrophoneStatus;
micVolume: number;
+ hasTranscripts: boolean;
onToggle: () => void;
- onReset: () => void;
+ onNew: () => void;
error?: string | null;
}
@@ -33,8 +34,9 @@ export function ControlPanel({
isConnected,
micStatus,
micVolume,
+ hasTranscripts,
onToggle,
- onReset,
+ onNew,
error,
}: ControlPanelProps) {
const statusText = getStatusText(isListening, micStatus);
@@ -125,10 +127,11 @@ export function ControlPanel({
{/* Buttons */}
<div className="flex gap-2">
<button
- onClick={onReset}
+ onClick={onNew}
className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors uppercase tracking-wide"
+ title={hasTranscripts ? "Save and start new session" : "Start new session"}
>
- Reset
+ New
</button>
<button
disabled