feat(tasks): text input + Honcho persistence
Manual task input box in TaskList component. Tasks persisted to Honcho as JSON preference on every mutation. Tasks loaded from Honcho on identify (session reconnect).
This commit is contained in:
@@ -284,6 +284,12 @@ async def gemini_voice_ws(websocket: WebSocket):
|
|||||||
# Push updated task list to frontend after any mutation
|
# Push updated task list to frontend after any mutation
|
||||||
if fn_name in ("add_task", "remove_task", "complete_task", "clear_completed_tasks"):
|
if fn_name in ("add_task", "remove_task", "complete_task", "clear_completed_tasks"):
|
||||||
await send_tasks_to_frontend()
|
await send_tasks_to_frontend()
|
||||||
|
# Persist to Honcho
|
||||||
|
try:
|
||||||
|
if kira_memory.enabled:
|
||||||
|
kira_memory.set_user_preference(user_id, "tasks", json.dumps(tasks))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Send tool response back to Gemini
|
# Send tool response back to Gemini
|
||||||
if tool_results:
|
if tool_results:
|
||||||
@@ -332,6 +338,14 @@ async def gemini_voice_ws(websocket: WebSocket):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Honcho error during identify: {e}")
|
logger.warning(f"Honcho error during identify: {e}")
|
||||||
prefs = {}
|
prefs = {}
|
||||||
|
# Load tasks from Honcho
|
||||||
|
try:
|
||||||
|
saved_tasks = prefs.get("tasks", "")
|
||||||
|
if saved_tasks:
|
||||||
|
tasks.clear()
|
||||||
|
tasks.extend(json.loads(saved_tasks))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
await websocket.send_json({
|
await websocket.send_json({
|
||||||
"type": "identified",
|
"type": "identified",
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
@@ -392,6 +406,19 @@ async def gemini_voice_ws(websocket: WebSocket):
|
|||||||
if msg_type == "ping":
|
if msg_type == "ping":
|
||||||
await websocket.send_json({"type": "pong"})
|
await websocket.send_json({"type": "pong"})
|
||||||
|
|
||||||
|
if msg_type == "add_task":
|
||||||
|
text = msg.get("text", "").strip()
|
||||||
|
if text:
|
||||||
|
result = execute_tool("add_task", {"text": text}, tasks)
|
||||||
|
await send_tasks_to_frontend()
|
||||||
|
# Persist to Honcho
|
||||||
|
try:
|
||||||
|
if kira_memory.enabled:
|
||||||
|
kira_memory.set_user_preference(user_id, "tasks", json.dumps(tasks))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
continue
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default function App() {
|
|||||||
startRecording,
|
startRecording,
|
||||||
stopRecording,
|
stopRecording,
|
||||||
tasks,
|
tasks,
|
||||||
|
addTask,
|
||||||
} = useConversation();
|
} = useConversation();
|
||||||
|
|
||||||
const [currentSceneId, setCurrentSceneId] = useState('cozy-room');
|
const [currentSceneId, setCurrentSceneId] = useState('cozy-room');
|
||||||
@@ -144,7 +145,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<Notes />
|
<Notes />
|
||||||
<TaskList tasks={tasks} />
|
<TaskList tasks={tasks} addTask={addTask} />
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<ChatBubble messages={messages} isKiraSpeaking={isKiraSpeaking} userName={userName} />
|
<ChatBubble messages={messages} isKiraSpeaking={isKiraSpeaking} userName={userName} />
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { Task } from '../hooks/useConversation';
|
import { Task } from '../hooks/useConversation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
|
addTask: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TaskList({ tasks }: Props) {
|
export default function TaskList({ tasks, addTask }: Props) {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
const pending = tasks.filter((t) => !t.completed);
|
const pending = tasks.filter((t) => !t.completed);
|
||||||
const done = tasks.filter((t) => t.completed);
|
const done = tasks.filter((t) => t.completed);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!input.trim()) return;
|
||||||
|
addTask(input.trim());
|
||||||
|
setInput('');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<h3 className="text-sm font-bold text-kira-plum mb-2 flex items-center gap-2">
|
<h3 className="text-sm font-bold text-kira-plum mb-2 flex items-center gap-2">
|
||||||
@@ -19,9 +29,25 @@ export default function TaskList({ tasks }: Props) {
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex gap-1.5 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="add a task..."
|
||||||
|
className="flex-1 bg-white/30 border border-kira-pink/20 rounded-lg px-2 py-1 text-xs text-kira-plum placeholder:text-kira-plum/30 focus:outline-none focus:border-kira-pink/50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-kira-pink/30 hover:bg-kira-pink/50 text-kira-plum text-xs px-2 py-1 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
{tasks.length === 0 && (
|
{tasks.length === 0 && (
|
||||||
<div className="text-xs text-kira-plum/30 text-center py-4">
|
<div className="text-xs text-kira-plum/30 text-center py-2">
|
||||||
tell Kira what you need to do!
|
tell Kira or type a task above!
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -331,5 +331,9 @@ export function useConversation() {
|
|||||||
startRecording,
|
startRecording,
|
||||||
stopRecording,
|
stopRecording,
|
||||||
tasks,
|
tasks,
|
||||||
|
addTask: (text: string) => {
|
||||||
|
if (!text.trim() || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
|
||||||
|
wsRef.current.send(JSON.stringify({ type: 'add_task', text: text.trim() }));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user