Fixed LLM chat function

This commit is contained in:
2026-02-19 14:47:17 +00:00
parent 3ae4abeb9f
commit b13252be71
6 changed files with 1395 additions and 83 deletions
+1189 -6
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -4,7 +4,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "concurrently \"vite\" \"npx tsx server.ts\"",
"dev:frontend": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
@@ -18,15 +19,17 @@
"@xenova/transformers": "^2.17.2",
"concurrently": "^9.2.1",
"d3": "^7.9.0",
"dotenv": "^17.3.1",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"express": "^4.21.0",
"framer-motion": "^11.15.0",
"fuse.js": "^7.1.0",
"lucide-react": "^0.468.0",
"express": "^4.21.0",
"nodemailer": "^6.9.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
+1
View File
@@ -1,3 +1,4 @@
import 'dotenv/config'
import express from 'express'
import nodemailer from 'nodemailer'
import path from 'path'
+111 -72
View File
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, useCallback, useMemo, type KeyboardEvent } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { MessageCircle, X, Send, Loader2 } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import {
sendChatMessage,
isLLMAvailable,
@@ -58,6 +59,7 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [inputValue, setInputValue] = useState('')
const [isStreaming, setIsStreaming] = useState(false)
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set())
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
@@ -195,11 +197,11 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
variants={panelVariants}
role="dialog"
aria-label="Chat with AI about Andy"
data-chat-panel
className="fixed z-[90] font-ui
inset-0 rounded-none max-md:z-[101]
md:inset-auto md:bottom-[88px] md:right-6 md:rounded-xl"
style={{
width: undefined,
background: 'var(--surface)',
border: '1px solid var(--border-light)',
boxShadow: 'var(--shadow-lg)',
@@ -211,7 +213,7 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
>
<style>{`
@media (min-width: 768px) {
[data-chat-panel] { width: 380px; max-height: 480px; }
[data-chat-panel] { width: clamp(380px, 30vw, 500px); height: calc(66vh); }
}
@media (max-width: 767px) {
[data-chat-panel] {
@@ -225,12 +227,12 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
}
`}</style>
<div
data-chat-panel
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
minHeight: 0,
}}
>
{/* Header */}
@@ -408,42 +410,118 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
overflow: 'hidden',
}}
>
<div style={{ padding: '10px 14px', whiteSpace: 'pre-wrap' }}>
{getDisplayText(msg.content)}
<div style={{ padding: '10px 14px', whiteSpace: msg.role === 'user' ? 'pre-wrap' : undefined }}>
{msg.role === 'assistant' ? (
<div className="chat-markdown">
<ReactMarkdown>{getDisplayText(msg.content)}</ReactMarkdown>
</div>
) : (
getDisplayText(msg.content)
)}
</div>
{referencedItems.length > 0 && (
<div
style={{
borderTop: '1px solid var(--border-light)',
padding: '6px 8px',
display: 'flex',
flexDirection: 'column',
gap: '2px',
}}
>
{referencedItems.map((item) => {
const IconComponent = iconByType[item.iconType]
const colorStyle = iconColorStyles[item.iconVariant]
{referencedItems.length > 0 && (() => {
const isExpanded = expandedItems.has(i)
const visibleItems = isExpanded ? referencedItems : referencedItems.slice(0, 3)
const hasMore = referencedItems.length > 3
return (
return (
<div
style={{
borderTop: '1px solid var(--border-light)',
padding: '6px 8px',
display: 'flex',
flexDirection: 'column',
gap: '2px',
}}
>
{visibleItems.map((item) => {
const IconComponent = iconByType[item.iconType]
const colorStyle = iconColorStyles[item.iconVariant]
return (
<button
key={item.id}
onClick={() => handleItemClick(item)}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 8px',
borderRadius: '6px',
border: 'none',
background: 'transparent',
cursor: 'pointer',
width: '100%',
textAlign: 'left',
transition: 'background-color 100ms ease-out',
fontSize: '12px',
fontFamily: 'inherit',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--accent-light)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
}}
>
<div
style={{
width: '22px',
height: '22px',
borderRadius: '5px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
background: colorStyle.background,
color: colorStyle.color,
}}
>
{IconComponent && <IconComponent size={12} />}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontWeight: 500,
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{item.title}
</div>
<div
style={{
fontSize: '11px',
color: 'var(--text-tertiary)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
marginTop: '-1px',
}}
>
{item.subtitle}
</div>
</div>
</button>
)
})}
{hasMore && !isExpanded && (
<button
key={item.id}
onClick={() => handleItemClick(item)}
onClick={() => setExpandedItems((prev) => new Set(prev).add(i))}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 8px',
padding: '5px 8px',
borderRadius: '6px',
border: 'none',
background: 'transparent',
cursor: 'pointer',
width: '100%',
fontSize: '11.5px',
fontFamily: 'inherit',
color: 'var(--accent)',
textAlign: 'left',
transition: 'background-color 100ms ease-out',
fontSize: '12px',
fontFamily: 'inherit',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--accent-light)'
@@ -452,51 +530,12 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
e.currentTarget.style.backgroundColor = 'transparent'
}}
>
<div
style={{
width: '22px',
height: '22px',
borderRadius: '5px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
background: colorStyle.background,
color: colorStyle.color,
}}
>
{IconComponent && <IconComponent size={12} />}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontWeight: 500,
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{item.title}
</div>
<div
style={{
fontSize: '11px',
color: 'var(--text-tertiary)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
marginTop: '-1px',
}}
>
{item.subtitle}
</div>
</div>
See {referencedItems.length - 3} more related items
</button>
)
})}
</div>
)}
)}
</div>
)
})()}
</div>
</div>
)
+86
View File
@@ -718,3 +718,89 @@ textarea:focus-visible {
}
}
/* ─── Chat markdown styles ─── */
.chat-markdown {
font-size: 13px;
line-height: 1.5;
word-wrap: break-word;
overflow-wrap: break-word;
}
.chat-markdown p {
margin: 0 0 0.5em;
}
.chat-markdown p:last-child {
margin-bottom: 0;
}
.chat-markdown strong {
font-weight: 600;
color: var(--text-primary);
}
.chat-markdown em {
font-style: italic;
}
.chat-markdown ul,
.chat-markdown ol {
margin: 0.25em 0 0.5em;
padding-left: 1.4em;
}
.chat-markdown li {
margin: 0.15em 0;
}
.chat-markdown code {
font-family: var(--font-geist-mono);
font-size: 0.9em;
padding: 0.15em 0.35em;
border-radius: 4px;
background: var(--border-light);
}
.chat-markdown pre {
margin: 0.4em 0;
padding: 0.6em 0.8em;
border-radius: 6px;
background: var(--border-light);
overflow-x: auto;
}
.chat-markdown pre code {
padding: 0;
background: none;
}
.chat-markdown h1,
.chat-markdown h2,
.chat-markdown h3 {
font-weight: 600;
margin: 0.6em 0 0.3em;
line-height: 1.3;
}
.chat-markdown h1 { font-size: 1.1em; }
.chat-markdown h2 { font-size: 1.05em; }
.chat-markdown h3 { font-size: 1em; }
.chat-markdown a {
color: var(--accent);
text-decoration: underline;
}
.chat-markdown blockquote {
margin: 0.4em 0;
padding: 0.2em 0 0.2em 0.8em;
border-left: 3px solid var(--border-light);
color: var(--text-secondary);
}
.chat-markdown hr {
margin: 0.5em 0;
border: none;
border-top: 1px solid var(--border-light);
}
+2 -2
View File
@@ -5,8 +5,8 @@ export interface ChatMessage {
content: string
}
export const LLM_MODEL = 'z-ai/glm-5'
export const LLM_DISPLAY_NAME = 'GLM-5'
export const LLM_MODEL = 'google/gemini-3-flash-preview'
export const LLM_DISPLAY_NAME = 'Gemini 3 Flash'
export function isLLMAvailable(): boolean {
return true