✨ chat history
This commit is contained in:
@@ -63,11 +63,6 @@
|
|||||||
line-height: 1.4;
|
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 {
|
.chat-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -82,22 +77,111 @@
|
|||||||
flex-shrink: 0;
|
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;
|
background: none;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 2px 7px;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding: 3px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s, border-color 0.15s;
|
transition: color 0.15s, border-color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-clear-btn:hover {
|
.chat-new-btn:hover {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-color: var(--text-muted);
|
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) ─── */
|
/* ── Pending attachment indicator (between tab bar and messages) ─── */
|
||||||
.chat-attachment-indicator {
|
.chat-attachment-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -9,9 +9,23 @@ import './Chat.css'
|
|||||||
|
|
||||||
type TabId = 'chat' | 'feedback'
|
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 {
|
export function ChatPanel(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
chatHistory,
|
chatHistory,
|
||||||
|
chatSessionsByFile,
|
||||||
|
activeSessionIdByFile,
|
||||||
isAILoading,
|
isAILoading,
|
||||||
aiError,
|
aiError,
|
||||||
activeFileContent,
|
activeFileContent,
|
||||||
@@ -26,14 +40,19 @@ export function ChatPanel(): JSX.Element {
|
|||||||
setAIError,
|
setAIError,
|
||||||
setAnnotations,
|
setAnnotations,
|
||||||
linkAnnotationsToMessage,
|
linkAnnotationsToMessage,
|
||||||
clearChat
|
newChat,
|
||||||
|
setActiveSession
|
||||||
} = useEditorStore()
|
} = useEditorStore()
|
||||||
|
|
||||||
const [tab, setTab] = useState<TabId>('chat')
|
const [tab, setTab] = useState<TabId>('chat')
|
||||||
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [pendingAttachmentCount, setPendingAttachmentCount] = useState(0)
|
const [pendingAttachmentCount, setPendingAttachmentCount] = useState(0)
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const prevAnnotationCountRef = useRef(annotations.length)
|
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)
|
// Auto-switch to Feedback tab the first time annotations appear (0 → >0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prev = prevAnnotationCountRef.current
|
const prev = prevAnnotationCountRef.current
|
||||||
@@ -103,6 +122,12 @@ export function ChatPanel(): JSX.Element {
|
|||||||
const allAnnotationsForFile: TextAnnotation[] =
|
const allAnnotationsForFile: TextAnnotation[] =
|
||||||
(activeFilePath ? annotationsByFile[activeFilePath]?.annotations : undefined) ?? []
|
(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 (
|
return (
|
||||||
<div className="chat-panel">
|
<div className="chat-panel">
|
||||||
{/* ── Tab bar ── */}
|
{/* ── Tab bar ── */}
|
||||||
@@ -122,15 +147,25 @@ export function ChatPanel(): JSX.Element {
|
|||||||
<span className="chat-tab-badge">{annotations.length}</span>
|
<span className="chat-tab-badge">{annotations.length}</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
</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 ── */}
|
{/* ── Pending-attachment indicator ── */}
|
||||||
{pendingAttachmentCount > 0 && (
|
{pendingAttachmentCount > 0 && (
|
||||||
<div className="chat-attachment-indicator">
|
<div className="chat-attachment-indicator">
|
||||||
@@ -142,6 +177,29 @@ export function ChatPanel(): JSX.Element {
|
|||||||
{/* ── Panel content ── */}
|
{/* ── Panel content ── */}
|
||||||
{tab === 'feedback' ? (
|
{tab === 'feedback' ? (
|
||||||
<FeedbackPanel />
|
<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}>
|
<div className="chat-messages" ref={scrollRef}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { FileNode, ChatMessage, TextAnnotation, AnalysisMode, RevisionMeta, AttachmentMeta } from '../types/editor'
|
import type { FileNode, ChatMessage, ChatSession, TextAnnotation, AnalysisMode, RevisionMeta, AttachmentMeta } from '../types/editor'
|
||||||
|
|
||||||
interface AnnotationFileState {
|
interface AnnotationFileState {
|
||||||
mode: AnalysisMode
|
mode: AnalysisMode
|
||||||
@@ -19,8 +19,9 @@ interface EditorState {
|
|||||||
setContent: (content: string) => void
|
setContent: (content: string) => void
|
||||||
markSaved: () => void
|
markSaved: () => void
|
||||||
|
|
||||||
// Chat - persisted per file path
|
// Chat - persisted per file path, multiple sessions per file
|
||||||
chatHistoryByFile: Record<string, ChatMessage[]>
|
chatSessionsByFile: Record<string, ChatSession[]>
|
||||||
|
activeSessionIdByFile: Record<string, string>
|
||||||
chatHistory: ChatMessage[]
|
chatHistory: ChatMessage[]
|
||||||
isAILoading: boolean
|
isAILoading: boolean
|
||||||
aiError: string | null
|
aiError: string | null
|
||||||
@@ -29,7 +30,8 @@ interface EditorState {
|
|||||||
appendToLastAssistantMessage: (chunk: string) => void
|
appendToLastAssistantMessage: (chunk: string) => void
|
||||||
setAILoading: (loading: boolean) => void
|
setAILoading: (loading: boolean) => void
|
||||||
setAIError: (error: string | null) => void
|
setAIError: (error: string | null) => void
|
||||||
clearChat: () => void
|
newChat: () => void
|
||||||
|
setActiveSession: (sessionId: string) => void
|
||||||
|
|
||||||
// Annotations (highlights in editor) — also persisted per file
|
// Annotations (highlights in editor) — also persisted per file
|
||||||
annotations: TextAnnotation[]
|
annotations: TextAnnotation[]
|
||||||
@@ -128,7 +130,10 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
isDirty: false,
|
isDirty: false,
|
||||||
setActiveFile: (path, content) => {
|
setActiveFile: (path, content) => {
|
||||||
const s = get()
|
const s = get()
|
||||||
const existing = s.chatHistoryByFile[path] ?? []
|
const sessions = s.chatSessionsByFile[path] ?? []
|
||||||
|
const activeId = s.activeSessionIdByFile[path]
|
||||||
|
const activeSession = sessions.find(sess => sess.id === activeId) ?? sessions[sessions.length - 1]
|
||||||
|
const existing = activeSession?.messages ?? []
|
||||||
const savedAnnotationState = s.annotationsByFile[path]
|
const savedAnnotationState = s.annotationsByFile[path]
|
||||||
set({
|
set({
|
||||||
activeFilePath: path,
|
activeFilePath: path,
|
||||||
@@ -143,7 +148,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -151,7 +157,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
setContent: (content) => set({ activeFileContent: content, isDirty: true }),
|
setContent: (content) => set({ activeFileContent: content, isDirty: true }),
|
||||||
markSaved: () => set({ isDirty: false }),
|
markSaved: () => set({ isDirty: false }),
|
||||||
|
|
||||||
chatHistoryByFile: {},
|
chatSessionsByFile: {},
|
||||||
|
activeSessionIdByFile: {},
|
||||||
chatHistory: [],
|
chatHistory: [],
|
||||||
isAILoading: false,
|
isAILoading: false,
|
||||||
aiError: null,
|
aiError: null,
|
||||||
@@ -160,17 +167,30 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
const msg: ChatMessage = { id: `user-${Date.now()}`, role: 'user', content: text, attachments }
|
const msg: ChatMessage = { id: `user-${Date.now()}`, role: 'user', content: text, attachments }
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const history = [...s.chatHistory, msg]
|
const history = [...s.chatHistory, msg]
|
||||||
const byFile = s.activeFilePath
|
if (!s.activeFilePath) return { chatHistory: history }
|
||||||
? { ...s.chatHistoryByFile, [s.activeFilePath]: history }
|
let sessions = s.chatSessionsByFile[s.activeFilePath] ?? []
|
||||||
: s.chatHistoryByFile
|
let activeId = s.activeSessionIdByFile[s.activeFilePath]
|
||||||
return { chatHistory: history, chatHistoryByFile: byFile }
|
// If no session exists yet, create the first one
|
||||||
|
if (sessions.length === 0 || !activeId) {
|
||||||
|
const newSession: ChatSession = { id: `session-${Date.now()}`, createdAt: Date.now(), messages: history }
|
||||||
|
return {
|
||||||
|
chatHistory: history,
|
||||||
|
chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: [newSession] },
|
||||||
|
activeSessionIdByFile: { ...s.activeSessionIdByFile, [s.activeFilePath]: newSession.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updatedSessions = sessions.map(sess =>
|
||||||
|
sess.id === activeId ? { ...sess, messages: history } : sess
|
||||||
|
)
|
||||||
|
return { chatHistory: history, chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: updatedSessions } }
|
||||||
})
|
})
|
||||||
scheduleSave(() => {
|
scheduleSave(() => {
|
||||||
const st = get()
|
const st = get()
|
||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -180,10 +200,12 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
const msg: ChatMessage = { id: `asst-${Date.now()}`, role: 'assistant', content: '' }
|
const msg: ChatMessage = { id: `asst-${Date.now()}`, role: 'assistant', content: '' }
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const history = [...s.chatHistory, msg]
|
const history = [...s.chatHistory, msg]
|
||||||
const byFile = s.activeFilePath
|
if (!s.activeFilePath) return { chatHistory: history }
|
||||||
? { ...s.chatHistoryByFile, [s.activeFilePath]: history }
|
const activeId = s.activeSessionIdByFile[s.activeFilePath]
|
||||||
: s.chatHistoryByFile
|
const sessions = (s.chatSessionsByFile[s.activeFilePath] ?? []).map(sess =>
|
||||||
return { chatHistory: history, chatHistoryByFile: byFile }
|
sess.id === activeId ? { ...sess, messages: history } : sess
|
||||||
|
)
|
||||||
|
return { chatHistory: history, chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: sessions } }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -194,10 +216,12 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
if (last?.role === 'assistant') {
|
if (last?.role === 'assistant') {
|
||||||
history[history.length - 1] = { ...last, content: last.content + chunk }
|
history[history.length - 1] = { ...last, content: last.content + chunk }
|
||||||
}
|
}
|
||||||
const byFile = s.activeFilePath
|
if (!s.activeFilePath) return { chatHistory: history }
|
||||||
? { ...s.chatHistoryByFile, [s.activeFilePath]: history }
|
const activeId = s.activeSessionIdByFile[s.activeFilePath]
|
||||||
: s.chatHistoryByFile
|
const sessions = (s.chatSessionsByFile[s.activeFilePath] ?? []).map(sess =>
|
||||||
return { chatHistory: history, chatHistoryByFile: byFile }
|
sess.id === activeId ? { ...sess, messages: history } : sess
|
||||||
|
)
|
||||||
|
return { chatHistory: history, chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: sessions } }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -210,7 +234,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -218,24 +243,41 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
setAIError: (aiError) => set({ aiError }),
|
setAIError: (aiError) => set({ aiError }),
|
||||||
|
|
||||||
clearChat: () => {
|
newChat: () => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const byFile = s.activeFilePath
|
if (!s.activeFilePath) return {}
|
||||||
? { ...s.chatHistoryByFile, [s.activeFilePath]: [] }
|
const newSession: ChatSession = { id: `session-${Date.now()}`, createdAt: Date.now(), messages: [] }
|
||||||
: s.chatHistoryByFile
|
const existingSessions = s.chatSessionsByFile[s.activeFilePath] ?? []
|
||||||
return { chatHistory: [], chatHistoryByFile: byFile }
|
return {
|
||||||
|
chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: [...existingSessions, newSession] },
|
||||||
|
activeSessionIdByFile: { ...s.activeSessionIdByFile, [s.activeFilePath]: newSession.id },
|
||||||
|
chatHistory: []
|
||||||
|
}
|
||||||
})
|
})
|
||||||
scheduleSave(() => {
|
scheduleSave(() => {
|
||||||
const st = get()
|
const st = get()
|
||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setActiveSession: (sessionId: string) => {
|
||||||
|
set((s) => {
|
||||||
|
if (!s.activeFilePath) return {}
|
||||||
|
const session = (s.chatSessionsByFile[s.activeFilePath] ?? []).find(sess => sess.id === sessionId)
|
||||||
|
if (!session) return {}
|
||||||
|
return {
|
||||||
|
activeSessionIdByFile: { ...s.activeSessionIdByFile, [s.activeFilePath]: sessionId },
|
||||||
|
chatHistory: session.messages
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
annotations: [],
|
annotations: [],
|
||||||
annotationsByFile: {},
|
annotationsByFile: {},
|
||||||
setAnnotations: (annotations) => {
|
setAnnotations: (annotations) => {
|
||||||
@@ -255,7 +297,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -273,7 +316,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -290,7 +334,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -316,7 +361,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -327,17 +373,20 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
const history = s.chatHistory.map(m =>
|
const history = s.chatHistory.map(m =>
|
||||||
m.id === messageId ? { ...m, annotationIds } : m
|
m.id === messageId ? { ...m, annotationIds } : m
|
||||||
)
|
)
|
||||||
const byFile = s.activeFilePath
|
if (!s.activeFilePath) return { chatHistory: history }
|
||||||
? { ...s.chatHistoryByFile, [s.activeFilePath]: history }
|
const activeId = s.activeSessionIdByFile[s.activeFilePath]
|
||||||
: s.chatHistoryByFile
|
const sessions = (s.chatSessionsByFile[s.activeFilePath] ?? []).map(sess =>
|
||||||
return { chatHistory: history, chatHistoryByFile: byFile }
|
sess.id === activeId ? { ...sess, messages: history } : sess
|
||||||
|
)
|
||||||
|
return { chatHistory: history, chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: sessions } }
|
||||||
})
|
})
|
||||||
scheduleSave(() => {
|
scheduleSave(() => {
|
||||||
const st = get()
|
const st = get()
|
||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -361,7 +410,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeFilePath: st.activeFilePath,
|
activeFilePath: st.activeFilePath,
|
||||||
scrollPositions: st.scrollPositions,
|
scrollPositions: st.scrollPositions,
|
||||||
chatHistoryByFile: st.chatHistoryByFile,
|
chatSessionsByFile: st.chatSessionsByFile,
|
||||||
|
activeSessionIdByFile: st.activeSessionIdByFile,
|
||||||
annotationsByFile: st.annotationsByFile
|
annotationsByFile: st.annotationsByFile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -402,8 +452,23 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
if (data.scrollPositions && typeof data.scrollPositions === 'object') {
|
if (data.scrollPositions && typeof data.scrollPositions === 'object') {
|
||||||
patch.scrollPositions = data.scrollPositions as Record<string, number>
|
patch.scrollPositions = data.scrollPositions as Record<string, number>
|
||||||
}
|
}
|
||||||
if (data.chatHistoryByFile && typeof data.chatHistoryByFile === 'object') {
|
if (data.chatSessionsByFile && typeof data.chatSessionsByFile === 'object') {
|
||||||
patch.chatHistoryByFile = data.chatHistoryByFile as Record<string, ChatMessage[]>
|
patch.chatSessionsByFile = data.chatSessionsByFile as Record<string, ChatSession[]>
|
||||||
|
patch.activeSessionIdByFile = (data.activeSessionIdByFile as Record<string, string>) ?? {}
|
||||||
|
} else if (data.chatHistoryByFile && typeof data.chatHistoryByFile === 'object') {
|
||||||
|
// Migrate from old flat format — wrap each file's history in a single session
|
||||||
|
const legacy = data.chatHistoryByFile as Record<string, ChatMessage[]>
|
||||||
|
const sessions: Record<string, ChatSession[]> = {}
|
||||||
|
const activeIds: Record<string, string> = {}
|
||||||
|
for (const [path, messages] of Object.entries(legacy)) {
|
||||||
|
if (Array.isArray(messages) && messages.length > 0) {
|
||||||
|
const id = `session-migrated-${Date.now()}`
|
||||||
|
sessions[path] = [{ id, createdAt: Date.now(), messages }]
|
||||||
|
activeIds[path] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patch.chatSessionsByFile = sessions
|
||||||
|
patch.activeSessionIdByFile = activeIds
|
||||||
}
|
}
|
||||||
if (data.annotationsByFile && typeof data.annotationsByFile === 'object') {
|
if (data.annotationsByFile && typeof data.annotationsByFile === 'object') {
|
||||||
patch.annotationsByFile = data.annotationsByFile as Record<string, AnnotationFileState>
|
patch.annotationsByFile = data.annotationsByFile as Record<string, AnnotationFileState>
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export interface ChatMessage {
|
|||||||
annotationIds?: string[] // IDs of suggestions this message produced
|
annotationIds?: string[] // IDs of suggestions this message produced
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatSession {
|
||||||
|
id: string
|
||||||
|
createdAt: number
|
||||||
|
messages: ChatMessage[]
|
||||||
|
}
|
||||||
|
|
||||||
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'critique' | 'custom'
|
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'critique' | 'custom'
|
||||||
|
|
||||||
export interface TextAnnotation {
|
export interface TextAnnotation {
|
||||||
|
|||||||
Reference in New Issue
Block a user