Background context component for frontend
This commit is contained in:
parent
2054450142
commit
609cbb78de
2 changed files with 302 additions and 1 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
CopilotKit,
|
CopilotKit,
|
||||||
useCoAgent,
|
useCoAgent,
|
||||||
|
|
@ -10,6 +11,7 @@ import { CopilotChat } from "@copilotkit/react-ui";
|
||||||
import "@copilotkit/react-ui/styles.css";
|
import "@copilotkit/react-ui/styles.css";
|
||||||
import CitationBlock from "./CitationBlock";
|
import CitationBlock from "./CitationBlock";
|
||||||
import DbInfo from "./DbInfo";
|
import DbInfo from "./DbInfo";
|
||||||
|
import SettingsPanel, { STORAGE_KEY } from "./SettingsPanel";
|
||||||
|
|
||||||
// Must match AGUI_STATE_KEY from haiku.rag.agents.chat
|
// Must match AGUI_STATE_KEY from haiku.rag.agents.chat
|
||||||
const AGUI_STATE_KEY = "haiku.rag.chat";
|
const AGUI_STATE_KEY = "haiku.rag.chat";
|
||||||
|
|
@ -36,6 +38,7 @@ interface ChatSessionState {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
citations: Citation[];
|
citations: Citation[];
|
||||||
qa_history: QAResponse[];
|
qa_history: QAResponse[];
|
||||||
|
initial_context: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AG-UI state is namespaced under AGUI_STATE_KEY
|
// AG-UI state is namespaced under AGUI_STATE_KEY
|
||||||
|
|
@ -131,6 +134,24 @@ function FileIcon() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SettingsIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ToolCallIndicator({
|
function ToolCallIndicator({
|
||||||
toolName,
|
toolName,
|
||||||
status,
|
status,
|
||||||
|
|
@ -336,7 +357,27 @@ function ToolCallIndicator({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatContent() {
|
function ChatContentInner({
|
||||||
|
initialContext,
|
||||||
|
setInitialContext,
|
||||||
|
}: {
|
||||||
|
initialContext: string;
|
||||||
|
setInitialContext: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleSaveContext = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setInitialContext(value);
|
||||||
|
if (value) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, value);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setInitialContext],
|
||||||
|
);
|
||||||
|
|
||||||
useCoAgent<AgentState>({
|
useCoAgent<AgentState>({
|
||||||
name: "chat_agent",
|
name: "chat_agent",
|
||||||
initialState: {
|
initialState: {
|
||||||
|
|
@ -344,6 +385,7 @@ function ChatContent() {
|
||||||
session_id: "",
|
session_id: "",
|
||||||
citations: [],
|
citations: [],
|
||||||
qa_history: [],
|
qa_history: [],
|
||||||
|
initial_context: initialContext || null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -425,6 +467,40 @@ function ChatContent() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.settings-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.settings-btn:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
.settings-btn.has-context {
|
||||||
|
background: #eff6ff;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
.settings-btn.has-context:hover {
|
||||||
|
background: #dbeafe;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
}
|
||||||
.chat-content {
|
.chat-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
@ -438,6 +514,17 @@ function ChatContent() {
|
||||||
`}</style>
|
`}</style>
|
||||||
<div className="chat-wrapper">
|
<div className="chat-wrapper">
|
||||||
<div className="chat-container">
|
<div className="chat-container">
|
||||||
|
<div className="chat-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`settings-btn ${initialContext ? "has-context" : ""}`}
|
||||||
|
onClick={() => setSettingsOpen(true)}
|
||||||
|
title={initialContext ? "Background context is set" : "Set background context"}
|
||||||
|
>
|
||||||
|
<SettingsIcon />
|
||||||
|
Context
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="chat-content">
|
<div className="chat-content">
|
||||||
<CopilotChat
|
<CopilotChat
|
||||||
labels={{
|
labels={{
|
||||||
|
|
@ -450,10 +537,36 @@ function ChatContent() {
|
||||||
<DbInfo />
|
<DbInfo />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<SettingsPanel
|
||||||
|
isOpen={settingsOpen}
|
||||||
|
onClose={() => setSettingsOpen(false)}
|
||||||
|
onSave={handleSaveContext}
|
||||||
|
currentValue={initialContext}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChatContent() {
|
||||||
|
const [initialContext, setInitialContext] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
setInitialContext(stored || "");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (initialContext === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatContentInner
|
||||||
|
initialContext={initialContext}
|
||||||
|
setInitialContext={setInitialContext}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
return (
|
return (
|
||||||
<CopilotKit runtimeUrl="/api/copilotkit" agent="chat_agent">
|
<CopilotKit runtimeUrl="/api/copilotkit" agent="chat_agent">
|
||||||
|
|
|
||||||
188
app/frontend/components/SettingsPanel.tsx
Normal file
188
app/frontend/components/SettingsPanel.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "haiku.rag.settings.initial_context";
|
||||||
|
|
||||||
|
interface SettingsPanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (initialContext: string) => void;
|
||||||
|
currentValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsPanel({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
currentValue,
|
||||||
|
}: SettingsPanelProps) {
|
||||||
|
const [value, setValue] = useState(currentValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setValue(currentValue);
|
||||||
|
}
|
||||||
|
}, [isOpen, currentValue]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
onSave(value);
|
||||||
|
onClose();
|
||||||
|
}, [value, onSave, onClose]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
.settings-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.settings-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.settings-modal-title {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
.settings-modal-description {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.settings-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.settings-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
.settings-textarea::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.settings-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
.settings-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.settings-btn-cancel {
|
||||||
|
background: white;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
.settings-btn-cancel:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.settings-btn-save {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.settings-btn-save:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: handled via onKeyDown on overlay */}
|
||||||
|
<div
|
||||||
|
className="settings-modal-overlay"
|
||||||
|
onClick={onClose}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="settings-title"
|
||||||
|
>
|
||||||
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: modal content wrapper */}
|
||||||
|
<div
|
||||||
|
className="settings-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h2 id="settings-title" className="settings-modal-title">
|
||||||
|
Background Context
|
||||||
|
</h2>
|
||||||
|
<p className="settings-modal-description">
|
||||||
|
Provide background information that will be used throughout your
|
||||||
|
conversation. This helps the assistant understand domain-specific
|
||||||
|
context, terminology, or any relevant details about your questions.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="settings-textarea"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
placeholder="e.g., Focus on Python programming concepts and best practices..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="settings-modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="settings-btn settings-btn-cancel"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="settings-btn settings-btn-save"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { STORAGE_KEY };
|
||||||
Loading…
Reference in a new issue