startConsensus(q, r, p, ms)}
- disabled={isActive}
+ onSubmit={(q, r, p, ms) => submitQuestion(q, r, p, ms)}
+ disabled={isActive || isRefining}
/>
{status === 'error' && error && (
@@ -31,6 +35,27 @@ export function ConsensusPanel() {
)}
+ {isRefining && clarifyingQuestions.length === 0 && (
+
+
+
+
+ Analyzing question...
+
+
+
+ )}
+
+ {isRefining && clarifyingQuestions.length > 0 && (
+
+ )}
+
{isComplete && decision && confidence !== null && (
)}
diff --git a/web/src/components/consensus/PhaseCard.tsx b/web/src/components/consensus/PhaseCard.tsx
index c2d54fb..03bc182 100644
--- a/web/src/components/consensus/PhaseCard.tsx
+++ b/web/src/components/consensus/PhaseCard.tsx
@@ -1,6 +1,7 @@
-import { GlassPanel, Markdown, Disclosure } from '@/components/shared'
+import { GlassPanel, Markdown, Disclosure, CitationList } from '@/components/shared'
import { ModelBadge } from './ModelBadge'
import { StreamingText } from './StreamingText'
+import type { Citation } from '@/api/types'
interface PhaseCardProps {
phase: string
@@ -8,13 +9,14 @@ interface PhaseCardProps {
models?: string[]
content?: string | null
isActive?: boolean
- challenges?: Array<{ model: string; content: string; truncated?: boolean; error?: boolean }>
+ challenges?: Array<{ model: string; content: string; truncated?: boolean; error?: boolean; citations?: Citation[] | null }>
collapsible?: boolean
defaultOpen?: boolean
truncated?: boolean
+ citations?: Citation[] | null
}
-export function PhaseCard({ phase, model, models, content, isActive, challenges, collapsible, defaultOpen = true, truncated }: PhaseCardProps) {
+export function PhaseCard({ phase, model, models, content, isActive, challenges, collapsible, defaultOpen = true, truncated, citations }: PhaseCardProps) {
const header = (
<>
{phase}
@@ -35,6 +37,9 @@ export function PhaseCard({ phase, model, models, content, isActive, challenges,
) : (
{content}
)}
+ {!isActive && citations && citations.length > 0 && (
+
+ )}
)}
@@ -54,6 +59,9 @@ export function PhaseCard({ phase, model, models, content, isActive, challenges,
>
{ch.error ? {ch.content} : {ch.content}}
+ {!ch.error && ch.citations && ch.citations.length > 0 && (
+
+ )}
))}
diff --git a/web/src/components/consensus/RefinementPanel.tsx b/web/src/components/consensus/RefinementPanel.tsx
new file mode 100644
index 0000000..14e9881
--- /dev/null
+++ b/web/src/components/consensus/RefinementPanel.tsx
@@ -0,0 +1,114 @@
+import { useState } from 'react'
+import { GlassPanel, GlowButton } from '@/components/shared'
+import type { ClarifyingQuestion } from '@/api/types'
+
+interface RefinementPanelProps {
+ questions: ClarifyingQuestion[]
+ answers: Record
+ onAnswer: (index: number, answer: string) => void
+ onSubmit: () => void
+ onSkip: () => void
+}
+
+export function RefinementPanel({
+ questions,
+ answers,
+ onAnswer,
+ onSubmit,
+ onSkip,
+}: RefinementPanelProps) {
+ const [activeTab, setActiveTab] = useState(0)
+ const allAnswered = questions.every((_, i) => (answers[i] ?? '').trim().length > 0)
+
+ const handleTextChange = (value: string) => {
+ onAnswer(activeTab, value)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Tab' && !e.shiftKey && (answers[activeTab] ?? '').trim()) {
+ const nextUnanswered = questions.findIndex(
+ (_, i) => i > activeTab && !(answers[i] ?? '').trim(),
+ )
+ if (nextUnanswered >= 0) {
+ e.preventDefault()
+ setActiveTab(nextUnanswered)
+ }
+ }
+ }
+
+ return (
+
+
+
+
+ Clarifying Questions
+
+
+
+ {/* Tab bar */}
+
+ {questions.map((_, i) => {
+ const answered = (answers[i] ?? '').trim().length > 0
+ const isActive = i === activeTab
+ return (
+
+ )
+ })}
+
+
+ {/* Active question */}
+ {questions[activeTab] && (
+
+
+ {questions[activeTab].question}
+
+ {questions[activeTab].hint && (
+
+ {questions[activeTab].hint}
+
+ )}
+
+ )}
+
+ {/* Footer */}
+
+
+ Skip
+
+
+ Start Consensus
+
+
+
+
+ )
+}
diff --git a/web/src/components/layout/Shell.tsx b/web/src/components/layout/Shell.tsx
index 089c67c..e731905 100644
--- a/web/src/components/layout/Shell.tsx
+++ b/web/src/components/layout/Shell.tsx
@@ -5,7 +5,8 @@ import { TopBar } from './TopBar'
import { GridOverlay, ParticleField } from '@/components/shared'
export function Shell() {
- const [sidebarOpen, setSidebarOpen] = useState(false)
+ const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
+ const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
return (
@@ -13,25 +14,37 @@ export function Shell() {
{/* Desktop sidebar */}
-
-
-
+ {desktopSidebarOpen && (
+
+ setDesktopSidebarOpen(false)} />
+
+ )}
{/* Mobile sidebar overlay */}
- {sidebarOpen && (
+ {mobileSidebarOpen && (
setSidebarOpen(false)}
+ onClick={() => setMobileSidebarOpen(false)}
/>
- setSidebarOpen(false)} />
+ setMobileSidebarOpen(false)} onToggleSidebar={() => setMobileSidebarOpen(false)} />
)}
-
setSidebarOpen(!sidebarOpen)} />
+ {
+ // Mobile: toggle mobile overlay; Desktop: reopen sidebar
+ if (window.innerWidth >= 1024) {
+ setDesktopSidebarOpen(true)
+ } else {
+ setMobileSidebarOpen(!mobileSidebarOpen)
+ }
+ }}
+ showSidebarToggle={!desktopSidebarOpen}
+ />
diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx
index db1c31b..512f149 100644
--- a/web/src/components/layout/Sidebar.tsx
+++ b/web/src/components/layout/Sidebar.tsx
@@ -1,4 +1,5 @@
-import { NavLink } from 'react-router-dom'
+import { NavLink, useNavigate } from 'react-router-dom'
+import { useConsensusStore } from '@/stores'
const navItems = [
{ path: '/', label: 'Consensus', icon: '\u2B21' },
@@ -8,16 +9,52 @@ const navItems = [
{ path: '/preferences', label: 'Preferences', icon: '\u2699' },
]
-export function Sidebar({ onClose }: { onClose?: () => void }) {
+export function Sidebar({ onClose, onToggleSidebar }: { onClose?: () => void; onToggleSidebar?: () => void }) {
+ const navigate = useNavigate()
+ const reset = useConsensusStore((s) => s.reset)
+
+ const handleNewQuestion = () => {
+ reset()
+ navigate('/')
+ onClose?.()
+ }
+
return (