Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,17 @@ ${userInputText}
]

// Add image parts back
for (const filePart of fileParts) {
contentParts.push({
type: "image",
image: filePart.url,
mimeType: filePart.mediaType,
})
const allowImages =
typeof modelId === "string" &&
!modelId.toLowerCase().includes("deepseek")
if (allowImages) {
for (const filePart of fileParts) {
contentParts.push({
type: "image",
image: filePart.url,
mimeType: filePart.mediaType,
})
}
}

enhancedMessages = [
Expand Down
29 changes: 20 additions & 9 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,11 @@
}

.animate-fade-in {
animation: fadeIn 0.3s ease-out forwards;
animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards;
animation: slideInRight 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

/* Message bubble animations */
Expand All @@ -228,22 +228,33 @@
}

.animate-message-in {
animation: messageIn 0.25s ease-out forwards;
animation: messageIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

/* Subtle floating shadow for cards */
.shadow-soft {
box-shadow:
0 1px 2px oklch(0.23 0.02 260 / 0.04),
0 4px 12px oklch(0.23 0.02 260 / 0.06),
0 8px 24px oklch(0.23 0.02 260 / 0.04);
0 2px 4px oklch(0.23 0.02 260 / 0.02),
0 6px 12px oklch(0.23 0.02 260 / 0.03),
0 12px 24px oklch(0.23 0.02 260 / 0.03);
}

.shadow-soft-lg {
box-shadow:
0 2px 4px oklch(0.23 0.02 260 / 0.04),
0 8px 20px oklch(0.23 0.02 260 / 0.08),
0 16px 40px oklch(0.23 0.02 260 / 0.06);
0 4px 8px oklch(0.23 0.02 260 / 0.03),
0 12px 24px oklch(0.23 0.02 260 / 0.05),
0 24px 48px oklch(0.23 0.02 260 / 0.04);
}

/* Gradient borders */
.border-gradient-primary {
position: relative;
background:
linear-gradient(var(--background), var(--background)) padding-box,
linear-gradient(135deg, oklch(0.55 0.18 265), oklch(0.6 0.2 290))
border-box;
border: 2px solid transparent;
border-radius: var(--radius);
}

/* Gradient text utility */
Expand Down
51 changes: 37 additions & 14 deletions components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface ValidationResult {
function validateFiles(
newFiles: File[],
existingCount: number,
imageSupported: boolean,
): ValidationResult {
const errors: string[] = []
const validFiles: File[] = []
Expand All @@ -74,7 +75,10 @@ function validateFiles(
errors.push(`Only ${availableSlots} more file(s) allowed`)
break
}
if (!isValidFileType(file)) {
if (
!isValidFileType(file) ||
(!imageSupported && file.type.startsWith("image/"))
) {
errors.push(`"${file.name}" is not a supported file type`)
continue
}
Expand Down Expand Up @@ -137,6 +141,7 @@ interface ChatInputProps {
error?: Error | null
minimalStyle?: boolean
onMinimalStyleChange?: (value: boolean) => void
imageSupported?: boolean
}

export function ChatInput({
Expand All @@ -154,6 +159,7 @@ export function ChatInput({
error = null,
minimalStyle = false,
onMinimalStyleChange = () => {},
imageSupported = true,
}: ChatInputProps) {
const {
diagramHistory,
Expand Down Expand Up @@ -224,6 +230,7 @@ export function ChatInput({
const { validFiles, errors } = validateFiles(
imageFiles,
files.length,
imageSupported,
)
showValidationErrors(errors)
if (validFiles.length > 0) {
Expand All @@ -234,7 +241,11 @@ export function ChatInput({

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newFiles = Array.from(e.target.files || [])
const { validFiles, errors } = validateFiles(newFiles, files.length)
const { validFiles, errors } = validateFiles(
newFiles,
files.length,
imageSupported,
)
showValidationErrors(errors)
if (validFiles.length > 0) {
onFileChange([...files, ...validFiles])
Expand Down Expand Up @@ -283,6 +294,7 @@ export function ChatInput({
const { validFiles, errors } = validateFiles(
supportedFiles,
files.length,
imageSupported,
)
showValidationErrors(errors)
if (validFiles.length > 0) {
Expand Down Expand Up @@ -333,7 +345,7 @@ export function ChatInput({
/>

{/* Action bar */}
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50 bg-muted/20 rounded-b-2xl">
{/* Left actions */}
<div className="flex items-center gap-1">
<ButtonWithTooltip
Expand All @@ -360,26 +372,26 @@ export function ChatInput({

<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-muted/50 transition-colors">
<Switch
id="minimal-style"
checked={minimalStyle}
onCheckedChange={onMinimalStyleChange}
className="scale-75"
className="scale-75 data-[state=checked]:bg-primary"
/>
<label
htmlFor="minimal-style"
className={`text-xs cursor-pointer select-none ${
className={`text-xs cursor-pointer select-none font-medium ${
minimalStyle
? "text-primary font-medium"
? "text-primary"
: "text-muted-foreground"
}`}
>
{minimalStyle ? "Minimal" : "Styled"}
</label>
</div>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side="top" className="text-xs">
Use minimal for faster generation (no colors)
</TooltipContent>
</Tooltip>
Expand All @@ -394,7 +406,7 @@ export function ChatInput({
onClick={() => onToggleHistory(true)}
disabled={isDisabled || diagramHistory.length === 0}
tooltipContent="Diagram history"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground transition-colors"
>
<History className="h-4 w-4" />
</ButtonWithTooltip>
Expand All @@ -406,7 +418,7 @@ export function ChatInput({
onClick={() => setShowSaveDialog(true)}
disabled={isDisabled}
tooltipContent="Save diagram"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground transition-colors"
>
<Download className="h-4 w-4" />
</ButtonWithTooltip>
Expand All @@ -428,8 +440,12 @@ export function ChatInput({
size="sm"
onClick={triggerFileInput}
disabled={isDisabled}
tooltipContent="Upload file (image, PDF, text)"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
tooltipContent={
imageSupported
? "Upload file (image, PDF, text)"
: "Upload file (PDF, text)"
}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground transition-colors"
>
<ImageIcon className="h-4 w-4" />
</ButtonWithTooltip>
Expand All @@ -448,9 +464,16 @@ export function ChatInput({

<Button
type="submit"
disabled={isDisabled || !input.trim()}
disabled={
isDisabled ||
(!input.trim() && files.length === 0)
}
size="sm"
className="h-8 px-4 rounded-xl font-medium shadow-sm"
className={`h-8 px-4 rounded-xl font-medium shadow-sm transition-all duration-300 ${
input.trim() || files.length > 0
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:scale-105"
: "bg-muted text-muted-foreground opacity-70"
}`}
aria-label={
isDisabled ? "Sending..." : "Send message"
}
Expand Down
10 changes: 5 additions & 5 deletions components/chat-message-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1006,15 +1006,15 @@ export function ChatMessageDisplay({
return (
<div
key={`${message.id}-content-${group.startIndex}`}
className={`px-4 py-3 text-sm leading-relaxed ${
className={`px-5 py-3.5 text-sm leading-7 shadow-sm transition-all duration-300 ${
message.role ===
"user"
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
? "bg-primary text-primary-foreground rounded-[20px] rounded-br-md ml-12"
: message.role ===
"system"
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""} ${groupIndex > 0 ? "mt-3" : ""}`}
? "bg-destructive/5 text-destructive border border-destructive/10 rounded-xl rounded-bl-md mr-12"
: "bg-background border border-border/50 text-foreground rounded-[20px] rounded-bl-md mr-12 shadow-soft"
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:brightness-110" : ""} ${groupIndex > 0 ? "mt-2" : ""}`}
role={
message.role ===
"user" &&
Expand Down
89 changes: 66 additions & 23 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { SettingsDialog } from "@/components/settings-dialog"
import { useDiagram } from "@/contexts/diagram-context"
import { getAIConfig } from "@/lib/ai-config"
import { findCachedResponse } from "@/lib/cached-responses"
import {
resolveImageSupport,
setCachedImageCapability,
} from "@/lib/model-capabilities"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
import { useQuotaManager } from "@/lib/use-quota-manager"
Expand Down Expand Up @@ -581,9 +585,15 @@ Continue from EXACTLY where you stopped.`,
"Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength."
}

// Translate image not supported error
if (friendlyMessage.includes("image content block")) {
if (
friendlyMessage.includes("image content block") ||
friendlyMessage.toLowerCase().includes("image_url") ||
friendlyMessage.toLowerCase().includes("unknown variant")
) {
friendlyMessage = "This model doesn't support image input."
// Cache capability as unsupported for current provider/model
const cfg = getAIConfig()
setCachedImageCapability(cfg.aiProvider, cfg.aiModel, false)
}

// Add system message for error so it can be cleared
Expand Down Expand Up @@ -959,7 +969,29 @@ Continue from EXACTLY where you stopped.`,
// Check all quota limits
if (!checkAllQuotaLimits()) return

sendChatMessage(parts, chartXml, previousXml, sessionId)
{
const config = getAIConfig()
const provider = config.aiProvider
const modelId = config.aiModel
const canUseImages = resolveImageSupport(provider, modelId)
const hasImage =
files.some((f) => !isPdfFile(f) && !isTextFile(f)) &&
parts.some((p) => p.type === "file")
if (hasImage && !canUseImages) {
const filtered = parts.filter((p) => p.type !== "file")
sendChatMessage(
filtered,
chartXml,
previousXml,
sessionId,
)
toast.warning(
"当前模型不支持图片输入,已忽略上传的图片",
)
} else {
sendChatMessage(parts, chartXml, previousXml, sessionId)
}
}

// Token count is tracked in onFinish with actual server usage
setInput("")
Expand Down Expand Up @@ -1234,18 +1266,18 @@ Continue from EXACTLY where you stopped.`,
// Collapsed view (desktop only)
if (!isVisible && !isMobile) {
return (
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl shadow-sm transition-all duration-300 ease-in-out">
<ButtonWithTooltip
tooltipContent="Show chat panel (Ctrl+B)"
variant="ghost"
size="icon"
onClick={onToggleVisibility}
className="hover:bg-accent transition-colors"
className="hover:bg-accent transition-colors rounded-xl"
>
<PanelRightOpen className="h-5 w-5 text-muted-foreground" />
</ButtonWithTooltip>
<div
className="text-sm font-medium text-muted-foreground mt-8 tracking-wide"
className="text-sm font-medium text-muted-foreground mt-8 tracking-wide opacity-80"
style={{
writingMode: "vertical-rl",
transform: "rotate(180deg)",
Expand All @@ -1259,7 +1291,8 @@ Continue from EXACTLY where you stopped.`,

// Full view
return (
<div className="h-full flex flex-col bg-card shadow-soft animate-slide-in-right rounded-xl border border-border/30 relative">
<div className="h-full flex flex-col bg-card/80 backdrop-blur-sm shadow-soft animate-slide-in-right rounded-xl border border-border/40 relative overflow-hidden transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-b from-background/50 to-transparent pointer-events-none" />
<Toaster
position="bottom-center"
richColors
Expand Down Expand Up @@ -1387,22 +1420,32 @@ Continue from EXACTLY where you stopped.`,
<footer
className={`${isMobile ? "p-2" : "p-4"} border-t border-border/50 bg-card/50`}
>
<ChatInput
input={input}
status={status}
onSubmit={onFormSubmit}
onChange={handleInputChange}
onClearChat={handleNewChat}
files={files}
onFileChange={handleFileChange}
pdfData={pdfData}
showHistory={showHistory}
onToggleHistory={setShowHistory}
sessionId={sessionId}
error={error}
minimalStyle={minimalStyle}
onMinimalStyleChange={setMinimalStyle}
/>
{(() => {
const cfg = getAIConfig()
const imgCap = resolveImageSupport(
cfg.aiProvider,
cfg.aiModel,
)
return (
<ChatInput
input={input}
status={status}
onSubmit={onFormSubmit}
onChange={handleInputChange}
onClearChat={handleNewChat}
files={files}
onFileChange={handleFileChange}
pdfData={pdfData}
showHistory={showHistory}
onToggleHistory={setShowHistory}
sessionId={sessionId}
error={error}
minimalStyle={minimalStyle}
onMinimalStyleChange={setMinimalStyle}
imageSupported={imgCap}
/>
)
})()}
</footer>

<SettingsDialog
Expand Down
Loading