✨ chat history
This commit is contained in:
@@ -63,11 +63,6 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Push Clear button to the far right inside the tab bar */
|
||||
.chat-tabs .chat-clear-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -82,22 +77,111 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-clear-btn {
|
||||
/* ── Chat sub-header (History link + New Chat button) ────────────── */
|
||||
.chat-subheader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-history-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 3px 0;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: transparent;
|
||||
}
|
||||
|
||||
.chat-history-link:hover,
|
||||
.chat-history-link-active {
|
||||
color: var(--text-primary);
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.chat-new-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
padding: 2px 7px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.chat-clear-btn:hover {
|
||||
.chat-new-btn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── History list ────────────────────────────────────────────────── */
|
||||
.chat-history-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.chat-history-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
padding: 9px 11px;
|
||||
border-radius: 6px;
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
transition: background 0.12s, border-color 0.12s;
|
||||
}
|
||||
|
||||
.chat-history-item:hover {
|
||||
background: var(--message-bg);
|
||||
}
|
||||
|
||||
.chat-history-item-active {
|
||||
border-color: var(--border);
|
||||
background: var(--message-bg);
|
||||
}
|
||||
|
||||
.chat-history-time {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.chat-history-summary {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.45;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.chat-history-item-active .chat-history-summary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Pending attachment indicator (between tab bar and messages) ─── */
|
||||
.chat-attachment-indicator {
|
||||
display: flex;
|
||||
|
||||
@@ -9,9 +9,23 @@ import './Chat.css'
|
||||
|
||||
type TabId = 'chat' | 'feedback'
|
||||
|
||||
function formatSessionTime(createdAt: number): string {
|
||||
const date = new Date(createdAt)
|
||||
const now = new Date()
|
||||
const isToday = date.toDateString() === now.toDateString()
|
||||
const yesterday = new Date(now)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const isYesterday = date.toDateString() === yesterday.toDateString()
|
||||
if (isToday) return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||
if (isYesterday) return 'Yesterday'
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
export function ChatPanel(): JSX.Element {
|
||||
const {
|
||||
chatHistory,
|
||||
chatSessionsByFile,
|
||||
activeSessionIdByFile,
|
||||
isAILoading,
|
||||
aiError,
|
||||
activeFileContent,
|
||||
@@ -26,14 +40,19 @@ export function ChatPanel(): JSX.Element {
|
||||
setAIError,
|
||||
setAnnotations,
|
||||
linkAnnotationsToMessage,
|
||||
clearChat
|
||||
newChat,
|
||||
setActiveSession
|
||||
} = useEditorStore()
|
||||
|
||||
const [tab, setTab] = useState<TabId>('chat')
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [pendingAttachmentCount, setPendingAttachmentCount] = useState(0)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const prevAnnotationCountRef = useRef(annotations.length)
|
||||
|
||||
// Collapse history when switching files
|
||||
useEffect(() => { setShowHistory(false) }, [activeFilePath])
|
||||
|
||||
// Auto-switch to Feedback tab the first time annotations appear (0 → >0)
|
||||
useEffect(() => {
|
||||
const prev = prevAnnotationCountRef.current
|
||||
@@ -103,6 +122,12 @@ export function ChatPanel(): JSX.Element {
|
||||
const allAnnotationsForFile: TextAnnotation[] =
|
||||
(activeFilePath ? annotationsByFile[activeFilePath]?.annotations : undefined) ?? []
|
||||
|
||||
const sessions = (activeFilePath ? chatSessionsByFile[activeFilePath] : undefined) ?? []
|
||||
const activeSessionId = activeFilePath ? activeSessionIdByFile[activeFilePath] : undefined
|
||||
|
||||
// Subheader is shown in the chat tab once there is at least one session
|
||||
const showSubheader = tab === 'chat' && sessions.length > 0
|
||||
|
||||
return (
|
||||
<div className="chat-panel">
|
||||
{/* ── Tab bar ── */}
|
||||
@@ -122,15 +147,25 @@ export function ChatPanel(): JSX.Element {
|
||||
<span className="chat-tab-badge">{annotations.length}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Clear button floated right, only visible in Chat tab */}
|
||||
{tab === 'chat' && chatHistory.length > 0 && (
|
||||
<button className="chat-clear-btn" onClick={clearChat} title="Clear conversation">
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Chat sub-header: History link + New Chat button ── */}
|
||||
{showSubheader && (
|
||||
<div className="chat-subheader">
|
||||
<button
|
||||
className={`chat-history-link${showHistory ? ' chat-history-link-active' : ''}`}
|
||||
onClick={() => setShowHistory(v => !v)}
|
||||
>
|
||||
{showHistory ? 'Back to chat' : `History${sessions.length > 1 ? ` (${sessions.length})` : ''}`}
|
||||
</button>
|
||||
{!showHistory && chatHistory.length > 0 && (
|
||||
<button className="chat-new-btn" onClick={newChat} title="Start a new conversation">
|
||||
+ New Chat
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Pending-attachment indicator ── */}
|
||||
{pendingAttachmentCount > 0 && (
|
||||
<div className="chat-attachment-indicator">
|
||||
@@ -142,6 +177,29 @@ export function ChatPanel(): JSX.Element {
|
||||
{/* ── Panel content ── */}
|
||||
{tab === 'feedback' ? (
|
||||
<FeedbackPanel />
|
||||
) : showHistory ? (
|
||||
<div className="chat-history-list">
|
||||
{[...sessions].reverse().map((session) => {
|
||||
const isActive = session.id === activeSessionId
|
||||
const firstUserMsg = session.messages.find(m => m.role === 'user')
|
||||
const summary = firstUserMsg
|
||||
? firstUserMsg.content.slice(0, 80) + (firstUserMsg.content.length > 80 ? '…' : '')
|
||||
: 'Empty conversation'
|
||||
return (
|
||||
<button
|
||||
key={session.id}
|
||||
className={`chat-history-item${isActive ? ' chat-history-item-active' : ''}`}
|
||||
onClick={() => {
|
||||
setActiveSession(session.id)
|
||||
setShowHistory(false)
|
||||
}}
|
||||
>
|
||||
<span className="chat-history-time">{formatSessionTime(session.createdAt)}</span>
|
||||
<span className="chat-history-summary">{summary}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="chat-messages" ref={scrollRef}>
|
||||
|
||||
Reference in New Issue
Block a user