127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import { useStore } from '@/lib/store';
|
|
import { renderMarkdown } from '@/lib/markdown';
|
|
|
|
export default function MessageList() {
|
|
const { messages, isLoading } = useStore();
|
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [messages, isLoading]);
|
|
|
|
if (!messages.length && !isLoading) {
|
|
return (
|
|
<div className="flex-1 flex flex-col items-center justify-center text-center px-8">
|
|
<h2 className="text-lg font-medium mb-2" style={{ color: 'var(--text2)' }}>
|
|
What are you working on?
|
|
</h2>
|
|
<p className="text-sm leading-relaxed max-w-sm" style={{ color: 'var(--text3)' }}>
|
|
Ask about your documents, projects, research, or anything else.
|
|
Your entire corpus is available.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex-1 overflow-y-auto px-4 py-6 flex flex-col gap-5">
|
|
{messages.map((m, i) => (
|
|
<div
|
|
key={i}
|
|
className={`flex flex-col max-w-3xl w-full ${
|
|
m.role === 'user' ? 'self-end items-end max-w-xl' : 'self-start items-start'
|
|
}`}
|
|
>
|
|
<div
|
|
className="text-xs mb-1"
|
|
style={{ color: 'var(--text3)', letterSpacing: '0.03em' }}
|
|
>
|
|
{m.role === 'user' ? 'you' : 'aaron ai'}
|
|
</div>
|
|
<div
|
|
className="px-4 py-3 leading-relaxed"
|
|
style={{
|
|
background: m.role === 'user' ? 'var(--user-bg)' : 'var(--accent-light)',
|
|
color: m.role === 'user' ? 'var(--text)' : 'var(--accent-text)',
|
|
border: m.role === 'assistant' ? '1px solid var(--accent-border)' : 'none',
|
|
borderRadius: m.role === 'user' ? '12px 12px 3px 12px' : '12px 12px 12px 3px',
|
|
fontSize: 'var(--font-size)',
|
|
}}
|
|
>
|
|
{m.role === 'assistant' ? (
|
|
<div
|
|
className="prose"
|
|
dangerouslySetInnerHTML={{ __html: renderMarkdown(m.content) }}
|
|
/>
|
|
) : (
|
|
<span style={{ whiteSpace: 'pre-wrap' }}>{m.content}</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 mt-1.5">
|
|
{m.role === 'assistant' && (
|
|
<button
|
|
onPointerUp={() => {
|
|
navigator.clipboard.writeText(m.content);
|
|
}}
|
|
style={{
|
|
background: 'none',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
padding: '2px 6px',
|
|
borderRadius: '4px',
|
|
color: 'var(--text3)',
|
|
fontSize: '11px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
}}
|
|
title="Copy response"
|
|
>
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
</svg>
|
|
Copy
|
|
</button>
|
|
)}
|
|
{m.sources && m.sources.length > 0 && (
|
|
<div className="text-xs italic" style={{ color: 'var(--text3)' }}>
|
|
Sources: {[...new Set(m.sources)].join(', ')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isLoading && (
|
|
<div className="self-start flex flex-col items-start max-w-3xl">
|
|
<div className="text-xs mb-1" style={{ color: 'var(--text3)' }}>aaron ai</div>
|
|
<div
|
|
className="px-4 py-3 rounded-xl text-sm italic"
|
|
style={{
|
|
background: 'var(--accent-light)',
|
|
color: 'var(--text3)',
|
|
border: '1px solid var(--accent-border)',
|
|
animation: 'pulse 1.5s infinite',
|
|
}}
|
|
>
|
|
Thinking...
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div ref={bottomRef} />
|
|
|
|
<style jsx>{`
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|