Fixed LLM chat function
This commit is contained in:
Generated
+1189
-6
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -4,7 +4,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "concurrently \"vite\" \"npx tsx server.ts\"",
|
||||||
|
"dev:frontend": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
@@ -18,15 +19,17 @@
|
|||||||
"@xenova/transformers": "^2.17.2",
|
"@xenova/transformers": "^2.17.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"embla-carousel-autoplay": "^8.6.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"express": "^4.21.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"express": "^4.21.0",
|
|
||||||
"nodemailer": "^6.9.0",
|
"nodemailer": "^6.9.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"react-markdown": "^10.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dotenv/config'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useRef, useEffect, useCallback, useMemo, type KeyboardEvent } from 'react'
|
import { useState, useRef, useEffect, useCallback, useMemo, type KeyboardEvent } from 'react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { MessageCircle, X, Send, Loader2 } from 'lucide-react'
|
import { MessageCircle, X, Send, Loader2 } from 'lucide-react'
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
import {
|
import {
|
||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
isLLMAvailable,
|
isLLMAvailable,
|
||||||
@@ -58,6 +59,7 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
|
|||||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
const [isStreaming, setIsStreaming] = useState(false)
|
const [isStreaming, setIsStreaming] = useState(false)
|
||||||
|
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set())
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
@@ -195,11 +197,11 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
|
|||||||
variants={panelVariants}
|
variants={panelVariants}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label="Chat with AI about Andy"
|
aria-label="Chat with AI about Andy"
|
||||||
|
data-chat-panel
|
||||||
className="fixed z-[90] font-ui
|
className="fixed z-[90] font-ui
|
||||||
inset-0 rounded-none max-md:z-[101]
|
inset-0 rounded-none max-md:z-[101]
|
||||||
md:inset-auto md:bottom-[88px] md:right-6 md:rounded-xl"
|
md:inset-auto md:bottom-[88px] md:right-6 md:rounded-xl"
|
||||||
style={{
|
style={{
|
||||||
width: undefined,
|
|
||||||
background: 'var(--surface)',
|
background: 'var(--surface)',
|
||||||
border: '1px solid var(--border-light)',
|
border: '1px solid var(--border-light)',
|
||||||
boxShadow: 'var(--shadow-lg)',
|
boxShadow: 'var(--shadow-lg)',
|
||||||
@@ -211,7 +213,7 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
|
|||||||
>
|
>
|
||||||
<style>{`
|
<style>{`
|
||||||
@media (min-width: 768px) {
|
@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) {
|
@media (max-width: 767px) {
|
||||||
[data-chat-panel] {
|
[data-chat-panel] {
|
||||||
@@ -225,12 +227,12 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
|
|||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<div
|
<div
|
||||||
data-chat-panel
|
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
minHeight: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -408,11 +410,22 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ padding: '10px 14px', whiteSpace: 'pre-wrap' }}>
|
<div style={{ padding: '10px 14px', whiteSpace: msg.role === 'user' ? 'pre-wrap' : undefined }}>
|
||||||
{getDisplayText(msg.content)}
|
{msg.role === 'assistant' ? (
|
||||||
|
<div className="chat-markdown">
|
||||||
|
<ReactMarkdown>{getDisplayText(msg.content)}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
getDisplayText(msg.content)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{referencedItems.length > 0 && (
|
{referencedItems.length > 0 && (() => {
|
||||||
|
const isExpanded = expandedItems.has(i)
|
||||||
|
const visibleItems = isExpanded ? referencedItems : referencedItems.slice(0, 3)
|
||||||
|
const hasMore = referencedItems.length > 3
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderTop: '1px solid var(--border-light)',
|
borderTop: '1px solid var(--border-light)',
|
||||||
@@ -422,7 +435,7 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
|
|||||||
gap: '2px',
|
gap: '2px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{referencedItems.map((item) => {
|
{visibleItems.map((item) => {
|
||||||
const IconComponent = iconByType[item.iconType]
|
const IconComponent = iconByType[item.iconType]
|
||||||
const colorStyle = iconColorStyles[item.iconVariant]
|
const colorStyle = iconColorStyles[item.iconVariant]
|
||||||
|
|
||||||
@@ -495,9 +508,35 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
{hasMore && !isExpanded && (
|
||||||
|
<button
|
||||||
|
onClick={() => setExpandedItems((prev) => new Set(prev).add(i))}
|
||||||
|
style={{
|
||||||
|
padding: '5px 8px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '11.5px',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
textAlign: 'left',
|
||||||
|
transition: 'background-color 100ms ease-out',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'var(--accent-light)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'transparent'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
See {referencedItems.length - 3} more related items
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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
@@ -5,8 +5,8 @@ export interface ChatMessage {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LLM_MODEL = 'z-ai/glm-5'
|
export const LLM_MODEL = 'google/gemini-3-flash-preview'
|
||||||
export const LLM_DISPLAY_NAME = 'GLM-5'
|
export const LLM_DISPLAY_NAME = 'Gemini 3 Flash'
|
||||||
|
|
||||||
export function isLLMAvailable(): boolean {
|
export function isLLMAvailable(): boolean {
|
||||||
return true
|
return true
|
||||||
|
|||||||
Reference in New Issue
Block a user