diff --git a/frontend/src/components/chat/HunterCompanion.tsx b/frontend/src/components/chat/HunterCompanion.tsx index 76f0f01..08ffeba 100644 --- a/frontend/src/components/chat/HunterCompanion.tsx +++ b/frontend/src/components/chat/HunterCompanion.tsx @@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation' import Link from 'next/link' import { MessageSquare, X, Send, Sparkles, Loader2, Lock, Trash2 } from 'lucide-react' import { useStore } from '@/lib/store' +import { buildUiHelpAnswer, isUiHelpQuestion } from '@/components/chat/terminalHelp' type Role = 'system' | 'user' | 'assistant' @@ -106,6 +107,7 @@ export function HunterCompanion() { const [error, setError] = useState(null) const listRef = useRef(null) + const inputRef = useRef(null) useEffect(() => { try { @@ -133,6 +135,12 @@ export function HunterCompanion() { el.scrollTop = el.scrollHeight }, [open, messages.length]) + useEffect(() => { + if (!open) return + // Focus input on open + setTimeout(() => inputRef.current?.focus(), 0) + }, [open]) + // Only show inside terminal routes if (!pathname?.startsWith('/terminal')) return null @@ -141,6 +149,21 @@ export function HunterCompanion() { if (!text || sending) return setError(null) + // Scout UI-help: answer locally (no LLM call) so Scout can learn the app. + if (!canChat && isUiHelpQuestion(text)) { + const userMsg: ChatMessage = { id: uid(), role: 'user', content: text, createdAt: Date.now() } + const assistantMsg: ChatMessage = { + id: uid(), + role: 'assistant', + content: buildUiHelpAnswer(pathname || '/terminal/hunt', text), + createdAt: Date.now(), + } + setMessages((prev) => [...prev, userMsg, assistantMsg]) + setInput('') + setTimeout(() => inputRef.current?.focus(), 0) + return + } + const userMsg: ChatMessage = { id: uid(), role: 'user', content: text, createdAt: Date.now() } const assistantMsgId = uid() setMessages((prev) => [...prev, userMsg, { id: assistantMsgId, role: 'assistant', content: '', createdAt: Date.now() }]) @@ -151,11 +174,11 @@ export function HunterCompanion() { { role: 'system' as const, content: - 'Du bist der Pounce Hunter Companion. Antworte natürlich und hilfreich auf Fragen (wie ein normaler Chat). Sprache: Deutsch. Ton: klar, freundlich, pragmatisch.\n\nWenn die Frage Domain‑bezogen ist, antworte zusätzlich strukturiert und entscheidungsorientiert:\n- Empfehlung: BUY / CONSIDER / SKIP\n- 3–6 Bulletpoints “Warum”\n- Wenn Informationen fehlen: stelle gezielte Rückfragen.\n\nWichtig: Behaupte niemals, dass du externe Quellen geprüft hast, außer der Nutzer hat dir die Daten gegeben.', + 'You are the Pounce Hunter Companion: a domain trading expert (auctions, drops, brandables, pricing, risk, SEO, and negotiation). Always respond in English. Be natural and helpful like a normal chat.\n\nIf the user’s question is domain-related, be decisional and structured:\n- Recommendation: BUY / CONSIDER / SKIP\n- 3–6 bullets “Why”\n- Ask targeted follow-up questions if key info is missing (budget, use-case, timeframe, TLD preferences).\n\nIf the user asks about how the app works, explain the UI and workflow step-by-step.\n\nCritical: Never claim you checked external sources unless the user provided the data.', }, { role: 'system' as const, - content: `Kontext: current_terminal_path=${pathname}; tier=${tier}.`, + content: `Context: current_terminal_path=${pathname}; tier=${tier}.`, }, ] @@ -166,7 +189,7 @@ export function HunterCompanion() { try { await streamChatCompletion({ messages: [...system, ...history, { role: 'user', content: text }], - temperature: 0.6, + temperature: 0.7, onDelta: (delta) => { setMessages((prev) => prev.map((m) => (m.id === assistantMsgId ? { ...m, content: (m.content || '') + delta } : m)) @@ -191,6 +214,8 @@ export function HunterCompanion() { ) } finally { setSending(false) + // Keep input focused after sending so the user can continue typing immediately + setTimeout(() => inputRef.current?.focus(), 0) } } @@ -313,6 +338,7 @@ export function HunterCompanion() {
setInput(e.target.value)} onKeyDown={(e) => { diff --git a/frontend/src/components/chat/terminalHelp.ts b/frontend/src/components/chat/terminalHelp.ts new file mode 100644 index 0000000..c11bf2a --- /dev/null +++ b/frontend/src/components/chat/terminalHelp.ts @@ -0,0 +1,196 @@ +export type TerminalHelpSection = { + title: string + bullets: string[] +} + +type HelpDoc = { + overview: string[] + sections: TerminalHelpSection[] +} + +// Keep this doc aligned with actual Terminal pages and UI. +// This is used for Scout UI-help (no LLM call) and as quick onboarding for all tiers. +export const TERMINAL_HELP: Record = { + '/terminal/hunt': { + overview: [ + 'The Hunt page is your discovery hub: auctions, drops, search, trends, and brandable generation.', + 'Use the tabs at the top to switch between engines.', + ], + sections: [ + { + title: 'Analyze a domain (right-side panel)', + bullets: [ + 'Click a domain name (where available) to open the Domain Analysis panel.', + 'Use the section tabs (Authority/Market/Risk/Value) to quickly evaluate.', + ], + }, + { + title: 'Workflow', + bullets: [ + 'Shortlist candidates → Analyze → Add to Watchlist/Portfolio → Set alerts → Execute.', + ], + }, + ], + }, + '/terminal/watchlist': { + overview: [ + 'Watchlist tracks domains you care about and monitors availability changes.', + 'Use it to follow targets and trigger the Analysis panel from the list.', + ], + sections: [ + { + title: 'Common actions', + bullets: [ + 'Click a domain to open Domain Analysis.', + 'Refresh / remove items to keep the list clean.', + ], + }, + ], + }, + '/terminal/portfolio': { + overview: [ + 'Portfolio is for domains you already own (costs, renewal timing, ROI signals).', + 'Add domains here to avoid “renewal death by a thousand cuts”.', + ], + sections: [ + { + title: 'CFO mindset', + bullets: [ + 'Track renewal dates and burn-rate.', + 'Drop low-signal names instead of carrying dead weight.', + ], + }, + ], + }, + '/terminal/market': { + overview: [ + 'Market is the feed for opportunities (external + Pounce sources).', + 'Use filters/sorting to reduce noise and move faster.', + ], + sections: [ + { + title: 'Speed', + bullets: [ + 'Filter by keyword/TLD/price and sort by score when available.', + ], + }, + ], + }, + '/terminal/intel': { + overview: [ + 'Intel is where you research TLD economics (pricing, renewal costs, history).', + 'Use it before you buy names with expensive renewals.', + ], + sections: [ + { + title: 'TLD detail', + bullets: [ + 'Open a TLD to view pricing and history (higher tiers may unlock more history windows).', + ], + }, + ], + }, + '/terminal/sniper': { + overview: [ + 'Sniper is your automated matching engine: alerts based on your criteria.', + 'It’s for catching opportunities without staring at feeds all day.', + ], + sections: [ + { + title: 'Best practice', + bullets: [ + 'Keep filters tight (budget, length, TLD) to avoid alert fatigue.', + ], + }, + ], + }, + '/terminal/listing': { + overview: [ + 'For Sale is Pounce Direct: list domains, verify ownership via DNS, manage leads.', + 'Use Leads to handle inquiries and negotiate.', + ], + sections: [ + { + title: 'Listing flow', + bullets: [ + 'Create listing → verify DNS ownership → publish → manage inquiries → close sale.', + ], + }, + ], + }, + '/terminal/inbox': { + overview: [ + 'Inbox is your buyer/seller communication hub for inquiries.', + 'Use Buying/Selling tabs to switch perspective.', + ], + sections: [ + { + title: 'Realtime', + bullets: [ + 'Threads and messages update via polling; unread counts show in navigation.', + ], + }, + ], + }, + '/terminal/yield': { + overview: [ + 'Yield is your monetization dashboard (feature is in development).', + 'You can activate domains and follow the DNS instructions when available.', + ], + sections: [ + { + title: 'Status', + bullets: [ + 'If something looks incomplete: it’s expected while the system is being built.', + ], + }, + ], + }, + '/terminal/settings': { + overview: [ + 'Settings is where you manage account and configuration.', + ], + sections: [], + }, +} + +function normalizePath(pathname: string): string { + if (!pathname) return '/terminal/hunt' + // Collapse dynamic subroutes to their base sections + if (pathname.startsWith('/terminal/intel/')) return '/terminal/intel' + return pathname +} + +export function isUiHelpQuestion(text: string): boolean { + const t = (text || '').toLowerCase() + // English + German common help intents + const keywords = [ + 'how', 'where', 'what does', 'explain', 'guide', 'help', 'tutorial', 'how to', 'workflow', 'feature', + 'wie', 'wo', 'was ist', 'erkläre', 'erklären', 'hilfe', 'tutorial', 'anleitung', 'funktion', 'workflow', + 'tab', 'seite', 'page', 'button', 'panel', 'sidebar', 'inbox', 'watchlist', 'portfolio', 'hunt', 'market', 'intel', 'sniper', 'listing', 'yield', + ] + return keywords.some((k) => t.includes(k)) +} + +export function buildUiHelpAnswer(pathname: string, question: string): string { + const p = normalizePath(pathname) + const doc = TERMINAL_HELP[p] || TERMINAL_HELP['/terminal/hunt'] + + const lines: string[] = [] + lines.push("Here’s how the UI works on this page:") + for (const o of doc.overview) lines.push(`- ${o}`) + + if (doc.sections.length) { + lines.push('') + for (const s of doc.sections) { + lines.push(`${s.title}:`) + for (const b of s.bullets) lines.push(`- ${b}`) + lines.push('') + } + } + + lines.push('If you tell me what you want to achieve (e.g. “find brandables under $50”), I’ll give you a step-by-step flow.') + return lines.join('\n').trim() +} + +