Introduces new API endpoints for AI chat and title generation, integrates an AI chat component into the layout, and updates client-side services to communicate with the new backend AI endpoints. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 64961019-b4a5-48d8-97fc-c4980d29f3c4 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/fhRML7y Replit-Helium-Checkpoint-Created: true
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import React from 'react';
|
|
import type { ChatMessage as ChatMessageType, Persona } from '@/lib/ai/types';
|
|
import { getPersonaIcon, UserIcon } from './Icons';
|
|
|
|
interface ChatMessageProps {
|
|
message: ChatMessageType;
|
|
persona: Persona;
|
|
}
|
|
|
|
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, persona }) => {
|
|
const isUser = message.role === 'user';
|
|
const Icon = getPersonaIcon(persona.icon);
|
|
|
|
const formatInlineCode = (text: string, keyPrefix: string, isUserMsg: boolean) => {
|
|
const codeRegex = /`([^`]+)`/g;
|
|
const parts = text.split(codeRegex);
|
|
|
|
return parts.map((part, i) => {
|
|
if (i % 2 === 1) {
|
|
const codeClass = isUserMsg
|
|
? "bg-black/20 text-white px-1.5 py-0.5 rounded text-sm font-mono"
|
|
: "bg-gray-900/50 text-cyan-300 px-1.5 py-0.5 rounded text-sm font-mono border border-gray-700/50";
|
|
return <code key={`${keyPrefix}-code-${i}`} className={codeClass}>{part}</code>;
|
|
}
|
|
|
|
const boldRegex = /\*\*(.*?)\*\*/g;
|
|
const boldParts = part.split(boldRegex);
|
|
|
|
return boldParts.map((bPart, bI) => {
|
|
if (bI % 2 === 1) {
|
|
return <strong key={`${keyPrefix}-bold-${i}-${bI}`} className="font-bold">{bPart}</strong>;
|
|
}
|
|
|
|
const italicRegex = /\*([^\*]+)\*/g;
|
|
const italicParts = bPart.split(italicRegex);
|
|
|
|
return italicParts.map((iPart, iI) => {
|
|
if (iI % 2 === 1) {
|
|
return <em key={`${keyPrefix}-italic-${i}-${bI}-${iI}`} className="italic opacity-90">{iPart}</em>;
|
|
}
|
|
return <span key={`${keyPrefix}-text-${i}-${bI}-${iI}`}>{iPart}</span>;
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
const formatContent = (content: string, isUserMsg: boolean) => {
|
|
const codeBlockRegex = /```([\s\S]*?)```/g;
|
|
const parts = content.split(codeBlockRegex);
|
|
|
|
return parts.map((part, index) => {
|
|
if (index % 2 === 1) {
|
|
const preClass = isUserMsg
|
|
? "bg-black/20 p-3 rounded-md overflow-x-auto my-2 text-white/90"
|
|
: "bg-gray-950 p-3 rounded-md overflow-x-auto my-2 border border-gray-800";
|
|
const codeClass = isUserMsg
|
|
? "text-sm font-mono"
|
|
: `text-sm font-mono ${persona.theme.primary}`;
|
|
|
|
return (
|
|
<pre key={index} className={preClass}>
|
|
<code className={codeClass}>{part.trim()}</code>
|
|
</pre>
|
|
);
|
|
}
|
|
|
|
const lines = part.split('\n');
|
|
return (
|
|
<div key={index} className="whitespace-pre-wrap leading-relaxed">
|
|
{lines.map((line, lineIdx) => {
|
|
const listMatch = line.match(/^(\s*)([-*]|\d+\.)\s+(.+)/);
|
|
|
|
if (listMatch) {
|
|
const [, , marker, text] = listMatch;
|
|
const isOrdered = /^\d+\./.test(marker);
|
|
return (
|
|
<div key={lineIdx} className="flex items-start gap-2 ml-2 mb-1">
|
|
<span className={`mt-1 text-xs opacity-70 flex-shrink-0 ${isOrdered ? '' : 'text-[8px] pt-1'}`}>
|
|
{isOrdered ? marker : '●'}
|
|
</span>
|
|
<span className="flex-1 min-w-0 break-words">
|
|
{formatInlineCode(text, `${index}-${lineIdx}`, isUserMsg)}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (line.trim() === '') {
|
|
return <div key={lineIdx} className="h-2" />;
|
|
}
|
|
|
|
return (
|
|
<div key={lineIdx} className="break-words min-w-0">
|
|
{formatInlineCode(line, `${index}-${lineIdx}`, isUserMsg)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
});
|
|
};
|
|
|
|
if (isUser) {
|
|
return (
|
|
<div className="flex justify-end items-start gap-3">
|
|
<div className="bg-primary/90 rounded-2xl rounded-tr-none p-3 md:p-4 max-w-[80%] shadow-lg">
|
|
<div className="text-primary-foreground text-sm md:text-base">
|
|
{formatContent(message.content, true)}
|
|
</div>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center flex-shrink-0 border border-border">
|
|
<UserIcon className="w-5 h-5 text-muted-foreground" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex justify-start items-start gap-3">
|
|
<div className={`w-10 h-10 rounded-full bg-gradient-to-tr ${persona.theme.avatar} flex items-center justify-center flex-shrink-0 shadow-lg`}>
|
|
<Icon className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div className="bg-card rounded-2xl rounded-tl-none p-3 md:p-4 max-w-[80%] shadow-lg border border-border">
|
|
<div className="text-card-foreground text-sm md:text-base">
|
|
{formatContent(message.content, false)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|