feat: Go backend, enhanced search, new widgets, Docker deploy

Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
This commit is contained in:
home
2026-02-27 04:15:32 +03:00
parent 328d968f3f
commit 06fe57c765
285 changed files with 53132 additions and 1871 deletions

View File

@@ -0,0 +1,275 @@
'use client';
import { useMemo, useState } from 'react';
import { motion } from 'framer-motion';
import ReactMarkdown from 'react-markdown';
import { User, Bot, Copy, Check, ExternalLink, ThumbsUp, ThumbsDown, Share2, Sparkles } from 'lucide-react';
import * as Tooltip from '@radix-ui/react-tooltip';
import type { Message, Citation as CitationType } from '@/lib/types';
interface ChatMessageProps {
message: Message;
}
export function ChatMessage({ message }: ChatMessageProps) {
const [copied, setCopied] = useState(false);
const [feedback, setFeedback] = useState<'up' | 'down' | null>(null);
const isUser = message.role === 'user';
const handleCopy = async () => {
await navigator.clipboard.writeText(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className={`group ${isUser ? 'flex justify-end' : ''}`}
>
{isUser ? (
<div className="max-w-[85%] flex items-start gap-3 flex-row-reverse">
<div className="w-9 h-9 rounded-xl bg-accent/10 border border-accent/20 flex items-center justify-center flex-shrink-0">
<User className="w-4 h-4 text-accent" />
</div>
<div className="bg-surface/60 border border-border/50 rounded-2xl rounded-tr-sm px-4 py-3">
<p className="text-[15px] text-primary leading-relaxed">{message.content}</p>
</div>
</div>
) : (
<div className="max-w-full">
{/* Assistant Header */}
<div className="flex items-center gap-2.5 mb-4">
<div className="w-9 h-9 rounded-xl bg-accent/10 border border-accent/20 flex items-center justify-center">
<Sparkles className="w-4 h-4 text-accent" />
</div>
<span className="text-sm font-medium text-secondary">GooSeek</span>
</div>
{/* Citations - Show at top like Perplexity */}
{message.citations && message.citations.length > 0 && (
<div className="mb-5">
<div className="flex flex-wrap gap-2">
{message.citations.slice(0, 6).map((citation) => (
<CitationBadge key={citation.index} citation={citation} />
))}
{message.citations.length > 6 && (
<button className="text-xs text-muted hover:text-secondary px-2.5 py-1.5 rounded-lg hover:bg-surface/40 transition-colors">
+{message.citations.length - 6} ещё
</button>
)}
</div>
</div>
)}
{/* Message Content */}
<div className="prose prose-invert max-w-none">
<ReactMarkdown
components={{
p: ({ children }) => (
<p className="mb-4 last:mb-0 text-[15px] text-primary/90 leading-[1.7]">
{children}
</p>
),
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:text-accent-hover underline underline-offset-2 decoration-accent/30 hover:decoration-accent/50 transition-colors"
>
{children}
</a>
),
code: ({ className, children }) => {
const isInline = !className;
if (isInline) {
return (
<code className="px-1.5 py-0.5 bg-surface/80 border border-border/50 rounded-md text-[13px] text-accent-secondary font-mono">
{children}
</code>
);
}
return <code className={className}>{children}</code>;
},
pre: ({ children }) => (
<pre className="bg-elevated/80 border border-border/50 p-4 rounded-xl overflow-x-auto text-[13px] my-5 font-mono">
{children}
</pre>
),
ul: ({ children }) => (
<ul className="list-none pl-0 my-4 space-y-2.5">{children}</ul>
),
ol: ({ children }) => (
<ol className="list-none pl-0 my-4 space-y-2.5 counter-reset-item">
{children}
</ol>
),
li: ({ children }) => (
<li className="flex gap-3 text-[15px] text-primary/85">
<span className="text-accent/60 select-none mt-0.5"></span>
<span className="leading-[1.6]">{children}</span>
</li>
),
h1: ({ children }) => (
<h1 className="text-xl font-semibold text-primary mt-8 mb-4">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-lg font-semibold text-primary mt-6 mb-3">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-base font-medium text-primary mt-5 mb-2">
{children}
</h3>
),
blockquote: ({ children }) => (
<blockquote className="border-l-2 border-accent/40 pl-4 my-5 text-secondary italic">
{children}
</blockquote>
),
strong: ({ children }) => (
<strong className="font-semibold text-primary">{children}</strong>
),
}}
>
{message.content}
</ReactMarkdown>
{/* Streaming Cursor */}
{message.isStreaming && (
<span className="inline-block w-2 h-5 bg-accent/60 rounded-sm animate-pulse-soft ml-1" />
)}
</div>
{/* Actions */}
{!message.isStreaming && (
<div className="flex items-center gap-1 mt-5 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<ActionButton
icon={copied ? Check : Copy}
label={copied ? 'Скопировано' : 'Копировать'}
onClick={handleCopy}
active={copied}
/>
<ActionButton
icon={Share2}
label="Поделиться"
onClick={() => {}}
/>
<div className="w-px h-4 bg-border/50 mx-1.5" />
<ActionButton
icon={ThumbsUp}
label="Полезно"
onClick={() => setFeedback('up')}
active={feedback === 'up'}
/>
<ActionButton
icon={ThumbsDown}
label="Не полезно"
onClick={() => setFeedback('down')}
active={feedback === 'down'}
/>
</div>
)}
</div>
)}
</motion.div>
);
}
interface ActionButtonProps {
icon: React.ElementType;
label: string;
onClick: () => void;
active?: boolean;
}
function ActionButton({ icon: Icon, label, onClick, active }: ActionButtonProps) {
return (
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
onClick={onClick}
className={`
p-2 rounded-lg transition-all
${active
? 'text-accent bg-accent/10'
: 'text-muted hover:text-secondary hover:bg-surface/50'
}
`}
>
<Icon className="w-4 h-4" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
side="top"
className="px-2.5 py-1.5 text-xs text-primary bg-surface border border-border rounded-lg shadow-dropdown z-50"
sideOffset={4}
>
{label}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
function CitationBadge({ citation }: { citation: CitationType }) {
return (
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<a
href={citation.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 bg-surface/40 hover:bg-surface/60 border border-border/50 hover:border-accent/30 rounded-xl transition-all group"
>
<span className="w-5 h-5 rounded-md bg-accent/15 text-accent flex items-center justify-center text-xs font-semibold">
{citation.index}
</span>
{citation.favicon && (
<img
src={citation.favicon}
alt=""
className="w-4 h-4 rounded"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
)}
<span className="text-sm text-secondary group-hover:text-primary max-w-[140px] truncate transition-colors">
{citation.domain}
</span>
<ExternalLink className="w-3 h-3 text-muted group-hover:text-secondary transition-colors" />
</a>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
side="top"
className="max-w-[300px] p-4 bg-surface/95 backdrop-blur-xl border border-border rounded-xl shadow-dropdown z-50"
sideOffset={8}
>
<p className="font-medium text-sm text-primary line-clamp-2 mb-2">
{citation.title}
</p>
{citation.snippet && (
<p className="text-xs text-secondary line-clamp-2 mb-2">
{citation.snippet}
</p>
)}
<p className="text-xs text-muted truncate">{citation.url}</p>
<Tooltip.Arrow className="fill-surface" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}