unify card/modal
This commit is contained in:
@@ -10,7 +10,6 @@ export function App() {
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [modalSession, setModalSession] = useState(null);
|
||||
const [conversations, setConversations] = useState({});
|
||||
const [conversationLoading, setConversationLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedProject, setSelectedProject] = useState(null);
|
||||
@@ -40,6 +39,12 @@ export function App() {
|
||||
// Track last_event_at for each session to detect actual changes
|
||||
const lastEventAtRef = useRef({});
|
||||
|
||||
// Refs for stable callback access (avoids recreation on state changes)
|
||||
const sessionsRef = useRef(sessions);
|
||||
const conversationsRef = useRef(conversations);
|
||||
sessionsRef.current = sessions;
|
||||
conversationsRef.current = conversations;
|
||||
|
||||
// Apply state payload from polling or SSE stream
|
||||
const applyStateData = useCallback((data) => {
|
||||
const newSessions = data.sessions || [];
|
||||
@@ -98,13 +103,9 @@ export function App() {
|
||||
}, [applyStateData]);
|
||||
|
||||
// Fetch conversation for a session
|
||||
const fetchConversation = useCallback(async (sessionId, projectDir, agent = 'claude', showLoading = false, force = false) => {
|
||||
const fetchConversation = useCallback(async (sessionId, projectDir, agent = 'claude', force = false) => {
|
||||
// Skip if already fetched and not forcing refresh
|
||||
if (!force && conversations[sessionId]) return;
|
||||
|
||||
if (showLoading) {
|
||||
setConversationLoading(true);
|
||||
}
|
||||
if (!force && conversationsRef.current[sessionId]) return;
|
||||
|
||||
try {
|
||||
let url = API_CONVERSATION + encodeURIComponent(sessionId);
|
||||
@@ -124,12 +125,8 @@ export function App() {
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Error fetching conversation:', err);
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
setConversationLoading(false);
|
||||
}
|
||||
}
|
||||
}, [conversations]);
|
||||
}, []);
|
||||
|
||||
// Respond to a session's pending question
|
||||
const respondToSession = useCallback(async (sessionId, text, isFreeform = false, optionCount = 0) => {
|
||||
@@ -146,13 +143,19 @@ export function App() {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
// Trigger refresh
|
||||
// Trigger state refresh
|
||||
fetchState();
|
||||
// Refresh conversation for immediate feedback
|
||||
const session = sessionsRef.current.find(s => s.session_id === sessionId);
|
||||
if (session) {
|
||||
await refreshConversationSilent(sessionId, session.project_dir, session.agent || 'claude');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error responding to session:', err);
|
||||
throw err; // Re-throw so SimpleInput/QuestionBlock can catch and show error
|
||||
}
|
||||
}, [fetchState]);
|
||||
}, [fetchState, refreshConversationSilent]);
|
||||
|
||||
// Dismiss a session
|
||||
const dismissSession = useCallback(async (sessionId) => {
|
||||
@@ -258,20 +261,9 @@ export function App() {
|
||||
setModalSession(session);
|
||||
|
||||
// Fetch conversation if not already cached
|
||||
if (!conversations[session.session_id]) {
|
||||
await fetchConversation(session.session_id, session.project_dir, session.agent || 'claude', true);
|
||||
if (!conversationsRef.current[session.session_id]) {
|
||||
await fetchConversation(session.session_id, session.project_dir, session.agent || 'claude');
|
||||
}
|
||||
}, [conversations, fetchConversation]);
|
||||
|
||||
// Refresh conversation (force re-fetch, used after sending messages)
|
||||
const refreshConversation = useCallback(async (sessionId, projectDir, agent = 'claude') => {
|
||||
// Force refresh by clearing cache first
|
||||
setConversations(prev => {
|
||||
const updated = { ...prev };
|
||||
delete updated[sessionId];
|
||||
return updated;
|
||||
});
|
||||
await fetchConversation(sessionId, projectDir, agent, false, true);
|
||||
}, [fetchConversation]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
@@ -383,10 +375,10 @@ export function App() {
|
||||
<${Modal}
|
||||
session=${modalSession}
|
||||
conversations=${conversations}
|
||||
conversationLoading=${conversationLoading}
|
||||
onClose=${handleCloseModal}
|
||||
onSendMessage=${respondToSession}
|
||||
onRefreshConversation=${refreshConversation}
|
||||
onFetchConversation=${fetchConversation}
|
||||
onRespond=${respondToSession}
|
||||
onDismiss=${dismissSession}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ import { html } from '../lib/preact.js';
|
||||
import { getUserMessageBg } from '../utils/status.js';
|
||||
import { MessageBubble, filterDisplayMessages } from './MessageBubble.js';
|
||||
|
||||
export function ChatMessages({ messages, status }) {
|
||||
export function ChatMessages({ messages, status, limit = 20 }) {
|
||||
const userBgClass = getUserMessageBg(status);
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
return html`
|
||||
<div class="flex h-full items-center justify-center rounded-xl border border-dashed border-selection/70 bg-bg/30 px-4 text-center text-sm text-dim">
|
||||
No messages yet
|
||||
No messages to show
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const allDisplayMessages = filterDisplayMessages(messages);
|
||||
const displayMessages = allDisplayMessages.slice(-20);
|
||||
const displayMessages = limit ? allDisplayMessages.slice(-limit) : allDisplayMessages;
|
||||
const offset = allDisplayMessages.length - displayMessages.length;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import { html, useState, useEffect, useRef, useCallback } from '../lib/preact.js';
|
||||
import { getStatusMeta, getUserMessageBg } from '../utils/status.js';
|
||||
import { formatDuration, formatTime } from '../utils/formatting.js';
|
||||
import { MessageBubble, filterDisplayMessages } from './MessageBubble.js';
|
||||
import { html, useState, useEffect, useCallback } from '../lib/preact.js';
|
||||
import { SessionCard } from './SessionCard.js';
|
||||
|
||||
export function Modal({ session, conversations, conversationLoading, onClose, onSendMessage, onRefreshConversation }) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [inputFocused, setInputFocused] = useState(false);
|
||||
export function Modal({ session, conversations, onClose, onRespond, onFetchConversation, onDismiss }) {
|
||||
const [closing, setClosing] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const wasAtBottomRef = useRef(true);
|
||||
const prevConversationLenRef = useRef(0);
|
||||
const chatContainerRef = useRef(null);
|
||||
|
||||
const conversation = session ? (conversations[session.session_id] || []) : [];
|
||||
|
||||
// Reset state when session changes
|
||||
// Reset closing state when session changes
|
||||
useEffect(() => {
|
||||
setClosing(false);
|
||||
prevConversationLenRef.current = 0;
|
||||
}, [session?.session_id]);
|
||||
|
||||
// Animated close handler
|
||||
@@ -30,40 +18,6 @@ export function Modal({ session, conversations, conversationLoading, onClose, on
|
||||
}, 200);
|
||||
}, [onClose]);
|
||||
|
||||
// Track scroll position
|
||||
useEffect(() => {
|
||||
const container = chatContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
const threshold = 50;
|
||||
wasAtBottomRef.current = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
return () => container.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
// Only scroll to bottom on NEW messages, and only if user was already at bottom
|
||||
useEffect(() => {
|
||||
const container = chatContainerRef.current;
|
||||
if (!container || !conversation) return;
|
||||
|
||||
const hasNewMessages = conversation.length > prevConversationLenRef.current;
|
||||
prevConversationLenRef.current = conversation.length;
|
||||
|
||||
if (hasNewMessages && wasAtBottomRef.current) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, [conversation]);
|
||||
|
||||
// Focus input when modal opens
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [session?.session_id]);
|
||||
|
||||
// Lock body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (!session) return;
|
||||
@@ -71,9 +25,9 @@ export function Modal({ session, conversations, conversationLoading, onClose, on
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [!!session]);
|
||||
}, [session?.session_id]);
|
||||
|
||||
// Handle keyboard events
|
||||
// Handle escape key
|
||||
useEffect(() => {
|
||||
if (!session) return;
|
||||
const handleKeyDown = (e) => {
|
||||
@@ -81,146 +35,26 @@ export function Modal({ session, conversations, conversationLoading, onClose, on
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [!!session, handleClose]);
|
||||
}, [session?.session_id, handleClose]);
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
const hasPendingQuestions = session.pending_questions && session.pending_questions.length > 0;
|
||||
const optionCount = hasPendingQuestions ? session.pending_questions[0]?.options?.length || 0 : 0;
|
||||
const status = getStatusMeta(session.status);
|
||||
const agent = session.agent === 'codex' ? 'codex' : 'claude';
|
||||
const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude';
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
const text = inputValue.trim();
|
||||
if (!text || sending) return;
|
||||
|
||||
setSending(true);
|
||||
try {
|
||||
if (onSendMessage) {
|
||||
await onSendMessage(session.session_id, text, true, optionCount);
|
||||
}
|
||||
setInputValue('');
|
||||
if (onRefreshConversation) {
|
||||
await onRefreshConversation(session.session_id, session.project_dir, agent);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send message:', err);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayMessages = filterDisplayMessages(conversation);
|
||||
const conversation = conversations[session.session_id] || [];
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-[#02050d]/84 p-4 backdrop-blur-sm ${closing ? 'modal-backdrop-out' : 'modal-backdrop-in'}"
|
||||
onClick=${(e) => e.target === e.currentTarget && handleClose()}
|
||||
>
|
||||
<div
|
||||
class="glass-panel flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-selection/80 ${closing ? 'modal-panel-out' : 'modal-panel-in'}"
|
||||
style=${{ borderLeftWidth: '3px', borderLeftColor: status.borderColor }}
|
||||
onClick=${(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between border-b px-5 py-4 ${agentHeaderClass}">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="mb-1 flex items-center gap-3">
|
||||
<h2 class="truncate font-display text-xl font-semibold text-bright">${session.project || session.name || session.session_id}</h2>
|
||||
<div class="flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs ${status.badge}">
|
||||
<span class="h-2 w-2 rounded-full ${status.dot} ${status.spinning ? 'spinner-dot' : ''}" style=${{ color: status.borderColor }}></span>
|
||||
<span class="font-mono uppercase tracking-[0.14em]">${status.label}</span>
|
||||
</div>
|
||||
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${agent === 'codex' ? 'border-emerald-400/45 bg-emerald-500/14 text-emerald-300' : 'border-violet-400/45 bg-violet-500/14 text-violet-300'}">
|
||||
${agent}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-sm text-dim">
|
||||
<span class="truncate font-mono">${session.cwd || 'No working directory'}</span>
|
||||
${session.started_at && html`
|
||||
<span class="flex-shrink-0 font-mono">Running ${formatDuration(session.started_at)}</span>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="ml-4 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl border border-selection/70 text-dim transition-colors duration-150 hover:border-done/35 hover:bg-done/10 hover:text-bright"
|
||||
onClick=${handleClose}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<div ref=${chatContainerRef} class="flex-1 overflow-y-auto overflow-x-hidden bg-surface p-5">
|
||||
${conversationLoading ? html`
|
||||
<div class="flex items-center justify-center py-12 animate-fade-in-up">
|
||||
<div class="font-mono text-dim">Loading conversation...</div>
|
||||
</div>
|
||||
` : displayMessages.length > 0 ? html`
|
||||
<div class="space-y-4">
|
||||
${displayMessages.map((msg, i) => html`
|
||||
<${MessageBubble}
|
||||
key=${`${msg.role}-${msg.timestamp || i}`}
|
||||
msg=${msg}
|
||||
userBg=${getUserMessageBg(session.status)}
|
||||
compact=${false}
|
||||
formatTime=${formatTime}
|
||||
/>
|
||||
`)}
|
||||
</div>
|
||||
` : html`
|
||||
<p class="text-dim text-center py-12">No conversation messages</p>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="border-t border-selection/70 bg-bg/55 p-4">
|
||||
${hasPendingQuestions && html`
|
||||
<div class="mb-3 rounded-xl border border-attention/40 bg-attention/12 px-3 py-2 text-sm text-attention">
|
||||
Agent is waiting for a response
|
||||
</div>
|
||||
`}
|
||||
<div class="flex items-end gap-2.5">
|
||||
<textarea
|
||||
ref=${inputRef}
|
||||
value=${inputValue}
|
||||
onInput=${(e) => {
|
||||
setInputValue(e.target.value);
|
||||
e.target.style.height = 'auto';
|
||||
e.target.style.height = Math.min(e.target.scrollHeight, 150) + 'px';
|
||||
}}
|
||||
onKeyDown=${handleInputKeyDown}
|
||||
onFocus=${() => setInputFocused(true)}
|
||||
onBlur=${() => setInputFocused(false)}
|
||||
placeholder=${hasPendingQuestions ? "Type your response..." : "Send a message..."}
|
||||
rows="1"
|
||||
class="flex-1 resize-none overflow-hidden rounded-xl border border-selection/75 bg-bg/70 px-4 py-2 text-sm text-fg transition-colors duration-150 placeholder:text-dim focus:outline-none"
|
||||
style=${{ minHeight: '42px', maxHeight: '150px', borderColor: inputFocused ? status.borderColor : undefined }}
|
||||
disabled=${sending}
|
||||
/>
|
||||
<button
|
||||
class="rounded-xl px-4 py-2 font-medium transition-[transform,filter] duration-150 hover:-translate-y-0.5 hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
style=${{ backgroundColor: status.borderColor, color: '#0a0f18' }}
|
||||
onClick=${handleSend}
|
||||
disabled=${sending || !inputValue.trim()}
|
||||
>
|
||||
${sending ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 font-mono text-label text-dim">
|
||||
Press Enter to send, Shift+Enter for new line, Escape to close
|
||||
</div>
|
||||
</div>
|
||||
<div class=${closing ? 'modal-panel-out' : 'modal-panel-in'} onClick=${(e) => e.stopPropagation()}>
|
||||
<${SessionCard}
|
||||
session=${session}
|
||||
conversation=${conversation}
|
||||
onFetchConversation=${onFetchConversation}
|
||||
onRespond=${onRespond}
|
||||
onDismiss=${onDismiss}
|
||||
enlarged=${true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { OptionButton } from './OptionButton.js';
|
||||
export function QuestionBlock({ questions, sessionId, status, onRespond }) {
|
||||
const [freeformText, setFreeformText] = useState('');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const meta = getStatusMeta(status);
|
||||
|
||||
if (!questions || questions.length === 0) return null;
|
||||
@@ -14,21 +16,45 @@ export function QuestionBlock({ questions, sessionId, status, onRespond }) {
|
||||
const remainingCount = questions.length - 1;
|
||||
const options = question.options || [];
|
||||
|
||||
const handleOptionClick = (optionLabel) => {
|
||||
onRespond(sessionId, optionLabel, false, options.length);
|
||||
const handleOptionClick = async (optionLabel) => {
|
||||
if (sending) return;
|
||||
setSending(true);
|
||||
setError(null);
|
||||
try {
|
||||
await onRespond(sessionId, optionLabel, false, options.length);
|
||||
} catch (err) {
|
||||
setError('Failed to send response');
|
||||
console.error('QuestionBlock option error:', err);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFreeformSubmit = (e) => {
|
||||
const handleFreeformSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (freeformText.trim()) {
|
||||
onRespond(sessionId, freeformText.trim(), true, options.length);
|
||||
setFreeformText('');
|
||||
if (freeformText.trim() && !sending) {
|
||||
setSending(true);
|
||||
setError(null);
|
||||
try {
|
||||
await onRespond(sessionId, freeformText.trim(), true, options.length);
|
||||
setFreeformText('');
|
||||
} catch (err) {
|
||||
setError('Failed to send response');
|
||||
console.error('QuestionBlock freeform error:', err);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="space-y-3" onClick=${(e) => e.stopPropagation()}>
|
||||
${error && html`
|
||||
<div class="rounded-lg border border-attention/40 bg-attention/12 px-3 py-1.5 text-sm text-attention">
|
||||
${error}
|
||||
</div>
|
||||
`}
|
||||
<!-- Question Header Badge -->
|
||||
${question.header && html`
|
||||
<span class="inline-flex rounded-full border px-2 py-1 font-mono text-micro uppercase tracking-[0.15em] ${meta.badge}">
|
||||
@@ -75,13 +101,15 @@ export function QuestionBlock({ questions, sessionId, status, onRespond }) {
|
||||
rows="1"
|
||||
class="flex-1 resize-none overflow-hidden rounded-xl border border-selection/75 bg-bg/70 px-3 py-2 text-sm text-fg transition-colors duration-150 placeholder:text-dim focus:outline-none"
|
||||
style=${{ minHeight: '38px', maxHeight: '150px', borderColor: focused ? meta.borderColor : undefined }}
|
||||
disabled=${sending}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="shrink-0 rounded-xl px-3 py-2 text-sm font-medium transition-[transform,filter] duration-150 hover:-translate-y-0.5 hover:brightness-110"
|
||||
class="shrink-0 rounded-xl px-3 py-2 text-sm font-medium transition-[transform,filter] duration-150 hover:-translate-y-0.5 hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
style=${{ backgroundColor: meta.borderColor, color: '#0a0f18' }}
|
||||
disabled=${sending || !freeformText.trim()}
|
||||
>
|
||||
Send
|
||||
${sending ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ChatMessages } from './ChatMessages.js';
|
||||
import { QuestionBlock } from './QuestionBlock.js';
|
||||
import { SimpleInput } from './SimpleInput.js';
|
||||
|
||||
export function SessionCard({ session, onClick, conversation, onFetchConversation, onRespond, onDismiss }) {
|
||||
export function SessionCard({ session, onClick, conversation, onFetchConversation, onRespond, onDismiss, enlarged = false }) {
|
||||
const hasQuestions = session.pending_questions && session.pending_questions.length > 0;
|
||||
const statusMeta = getStatusMeta(session.status);
|
||||
const agent = session.agent === 'codex' ? 'codex' : 'claude';
|
||||
@@ -20,23 +20,51 @@ export function SessionCard({ session, onClick, conversation, onFetchConversatio
|
||||
}, [session.session_id, session.project_dir, agent, conversation, onFetchConversation]);
|
||||
|
||||
const chatPaneRef = useRef(null);
|
||||
const wasAtBottomRef = useRef(true);
|
||||
const prevConversationLenRef = useRef(0);
|
||||
|
||||
// Scroll chat pane to bottom when conversation loads or updates
|
||||
// Track scroll position for smart scrolling
|
||||
useEffect(() => {
|
||||
const el = chatPaneRef.current;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
if (!el) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
const threshold = 50;
|
||||
wasAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
||||
};
|
||||
|
||||
el.addEventListener('scroll', handleScroll);
|
||||
return () => el.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
// Smart scroll: only scroll to bottom on new messages if user was already at bottom
|
||||
useEffect(() => {
|
||||
const el = chatPaneRef.current;
|
||||
if (!el || !conversation) return;
|
||||
|
||||
const hasNewMessages = conversation.length > prevConversationLenRef.current;
|
||||
prevConversationLenRef.current = conversation.length;
|
||||
|
||||
if (hasNewMessages && wasAtBottomRef.current) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}, [conversation]);
|
||||
|
||||
const handleDismissClick = (e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss(session.session_id);
|
||||
if (onDismiss) onDismiss(session.session_id);
|
||||
};
|
||||
|
||||
// Container classes differ based on enlarged mode
|
||||
const containerClasses = enlarged
|
||||
? 'glass-panel flex w-full max-w-[90vw] max-h-[90vh] flex-col overflow-hidden rounded-2xl border border-selection/80'
|
||||
: 'glass-panel flex h-[850px] max-h-[850px] w-[600px] cursor-pointer flex-col overflow-hidden rounded-xl border border-selection/70 transition-[border-color,box-shadow] duration-200 hover:border-starting/35 hover:shadow-panel';
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="glass-panel flex h-[850px] max-h-[850px] w-[600px] cursor-pointer flex-col overflow-hidden rounded-xl border border-selection/70 transition-[border-color,box-shadow] duration-200 hover:border-starting/35 hover:shadow-panel"
|
||||
class=${containerClasses}
|
||||
style=${{ borderLeftWidth: '3px', borderLeftColor: statusMeta.borderColor }}
|
||||
onClick=${() => onClick(session)}
|
||||
onClick=${enlarged ? undefined : () => onClick && onClick(session)}
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="shrink-0 border-b px-4 py-3 ${agentHeaderClass}">
|
||||
@@ -86,7 +114,7 @@ export function SessionCard({ session, onClick, conversation, onFetchConversatio
|
||||
|
||||
<!-- Card Content Area (Chat) -->
|
||||
<div ref=${chatPaneRef} class="flex-1 min-h-0 overflow-y-auto bg-surface px-4 py-4">
|
||||
<${ChatMessages} messages=${conversation || []} status=${session.status} />
|
||||
<${ChatMessages} messages=${conversation || []} status=${session.status} limit=${enlarged ? null : 20} />
|
||||
</div>
|
||||
|
||||
<!-- Card Footer (Input or Questions) -->
|
||||
|
||||
@@ -4,19 +4,36 @@ import { getStatusMeta } from '../utils/status.js';
|
||||
export function SimpleInput({ sessionId, status, onRespond }) {
|
||||
const [text, setText] = useState('');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const meta = getStatusMeta(status);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (text.trim()) {
|
||||
onRespond(sessionId, text.trim(), true, 0);
|
||||
setText('');
|
||||
if (text.trim() && !sending) {
|
||||
setSending(true);
|
||||
setError(null);
|
||||
try {
|
||||
await onRespond(sessionId, text.trim(), true, 0);
|
||||
setText('');
|
||||
} catch (err) {
|
||||
setError('Failed to send message');
|
||||
console.error('SimpleInput send error:', err);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<form onSubmit=${handleSubmit} class="flex items-end gap-2.5" onClick=${(e) => e.stopPropagation()}>
|
||||
<form onSubmit=${handleSubmit} class="flex flex-col gap-2" onClick=${(e) => e.stopPropagation()}>
|
||||
${error && html`
|
||||
<div class="rounded-lg border border-attention/40 bg-attention/12 px-3 py-1.5 text-sm text-attention">
|
||||
${error}
|
||||
</div>
|
||||
`}
|
||||
<div class="flex items-end gap-2.5">
|
||||
<textarea
|
||||
value=${text}
|
||||
onInput=${(e) => {
|
||||
@@ -36,14 +53,17 @@ export function SimpleInput({ sessionId, status, onRespond }) {
|
||||
rows="1"
|
||||
class="flex-1 resize-none overflow-hidden rounded-xl border border-selection/75 bg-bg/70 px-3 py-2 text-sm text-fg transition-colors duration-150 placeholder:text-dim focus:outline-none"
|
||||
style=${{ minHeight: '38px', maxHeight: '150px', borderColor: focused ? meta.borderColor : undefined }}
|
||||
disabled=${sending}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="shrink-0 rounded-xl px-3 py-2 text-sm font-medium transition-[transform,filter] duration-150 hover:-translate-y-0.5 hover:brightness-110"
|
||||
class="shrink-0 rounded-xl px-3 py-2 text-sm font-medium transition-[transform,filter] duration-150 hover:-translate-y-0.5 hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
style=${{ backgroundColor: meta.borderColor, color: '#0a0f18' }}
|
||||
disabled=${sending || !text.trim()}
|
||||
>
|
||||
Send
|
||||
${sending ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user