diff --git a/backend/app/services/llm_agent.py b/backend/app/services/llm_agent.py index fbac42d..9999a30 100644 --- a/backend/app/services/llm_agent.py +++ b/backend/app/services/llm_agent.py @@ -63,31 +63,23 @@ async def _get_user_tier(db: AsyncSession, user: User) -> str: def _build_system_prompt(path: str) -> str: tools = tool_catalog_for_prompt(path) return ( - "You are the Pounce Hunter Companion (domain trading expert). Always respond in English.\n" - "You have access to internal tools that return live data. Use tools when needed.\n\n" - "OUTPUT STYLE:\n" - "- Never show raw tool output to the user.\n" - "- Never print phrases like 'Tool Result', 'TOOL_RESULT', or code-fenced JSON.\n" - "- If you used tools, silently incorporate the data and present ONLY a clean summary.\n" - "- Keep formatting simple: short paragraphs and bullet points. Avoid dumping structured data.\n\n" + "You are the Pounce Hunter Companion, an expert domain trading assistant. Always respond in English.\n" + "You help users with: domain analysis, auction hunting, portfolio management, and trading decisions.\n\n" + "RESPONSE FORMAT (CRITICAL):\n" + "- Write in plain text. NO markdown asterisks, NO ** or *, NO code blocks.\n" + "- Use simple dashes (-) for bullet points.\n" + "- Keep responses concise: 2-4 sentences intro, then bullets if needed.\n" + "- Never show tool outputs, JSON, or internal data to the user.\n\n" "BEHAVIOR:\n" - "- Do NOT invent user preferences, keywords, TLDs, budgets, or tasks.\n" - "- If the user greets you or sends a minimal message (e.g., 'hi', 'hello'), respond naturally and ask what they want help with.\n" - "- Ask 1–2 clarifying questions when the user request is ambiguous.\n\n" - "WHEN TO USE TOOLS:\n" - "- Use tools only when the user explicitly asks about their account data, their current page, their lists (watchlist/portfolio/listings/inbox/yield), or when they provide a specific domain to analyze.\n" - "- Never proactively 'fetch' domains or run scans based on guessed keywords.\n\n" - "TOOL CALLING PROTOCOL:\n" - "- If you need data, respond with ONLY a JSON object:\n" - ' {"tool_calls":[{"name":"tool_name","args":{...}}, ...]}\n' - "- Do not include any other text when requesting tools.\n" - "- After tools are executed, you will receive TOOL_RESULT messages.\n" - "- When you are ready to answer the user, respond normally (not JSON) and do NOT mention tools.\n\n" - "AVAILABLE TOOLS (JSON schemas):\n" - f"{json.dumps(tools, ensure_ascii=False)}\n\n" - "RULES:\n" - "- Never claim you checked external sources unless the user provided the data.\n" - "- Keep answers practical and decisive. If (and only if) the user is asking about a specific domain: include BUY/CONSIDER/SKIP + bullets.\n" + "- Be helpful, direct, and conversational like a knowledgeable colleague.\n" + "- For domain questions: give a clear BUY / CONSIDER / SKIP recommendation with 3-5 reasons.\n" + "- Do NOT invent user preferences, keywords, or data. Ask if unclear.\n" + "- For greetings: respond naturally and ask how you can help.\n\n" + "TOOL USAGE:\n" + "- Use tools when user asks about their data (watchlist, portfolio, listings, inbox, yield) or a specific domain.\n" + "- To call tools, respond with ONLY: {\"tool_calls\":[{\"name\":\"...\",\"args\":{...}}]}\n" + "- After receiving tool results, answer naturally without mentioning tools.\n\n" + f"AVAILABLE TOOLS:\n{json.dumps(tools, ensure_ascii=False)}\n" ) @@ -151,11 +143,13 @@ async def run_agent( { "role": "assistant", "content": ( - "Hey — how can I help?\n\n" - "If you want, tell me:\n" - "- which Terminal page you’re on (or what you’re trying to do)\n" - "- or a specific domain you’re considering\n" - "- or what outcome you want (find deals, assess a name, manage leads, etc.)" + "Hey! How can I help you today?\n\n" + "I can help with:\n" + "- Analyzing a specific domain\n" + "- Finding auction deals or drops\n" + "- Reviewing your portfolio or watchlist\n" + "- Checking your listings and leads\n\n" + "Just tell me what you need." ), } ) @@ -212,11 +206,11 @@ async def stream_final_answer(convo: list[dict[str, Any]], *, model: Optional[st { "role": "system", "content": ( - "Final step: respond to the user.\n" - "- Do NOT output JSON tool_calls.\n" - "- Do NOT request tools.\n" - "- Do NOT include raw tool outputs, internal tags, or code-fenced JSON.\n" - "- If you used tools, present only a clean human summary." + "Final step: respond to the user in plain text.\n" + "- NO markdown: no ** or * for bold/italic, no code blocks.\n" + "- Use dashes (-) for bullets.\n" + "- Do NOT output JSON or mention tools.\n" + "- Be concise and helpful." ), } ], diff --git a/frontend/src/components/chat/HunterCompanion.tsx b/frontend/src/components/chat/HunterCompanion.tsx index 11facff..f47b410 100644 --- a/frontend/src/components/chat/HunterCompanion.tsx +++ b/frontend/src/components/chat/HunterCompanion.tsx @@ -4,9 +4,21 @@ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { usePathname } from 'next/navigation' import Link from 'next/link' -import { MessageSquare, X, Send, Sparkles, Loader2, Lock, Trash2 } from 'lucide-react' +import { + MessageSquare, + X, + Send, + Sparkles, + Loader2, + Lock, + Trash2, + TrendingUp, + Search, + Briefcase, + Target, + Zap, +} from 'lucide-react' import { useStore } from '@/lib/store' -import { buildUiHelpAnswer, isUiHelpQuestion } from '@/components/chat/terminalHelp' type Role = 'system' | 'user' | 'assistant' @@ -29,31 +41,26 @@ function getApiBase(): string { return `${protocol}//${hostname}/api/v1` } -async function streamChatCompletion(opts: { - model?: string +async function streamChat(opts: { messages: Array<{ role: Role; content: string }> - temperature?: number + path: string onDelta: (delta: string) => void - path?: string }): Promise { - // Use tool-calling agent for Trader/Tycoon. (Scout stays UI-help only.) const res = await fetch(`${getApiBase()}/llm/agent`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: opts.model, messages: opts.messages, - temperature: opts.temperature, + temperature: 0.5, stream: true, - path: opts.path || '/terminal/hunt', + path: opts.path, }), }) if (!res.ok) { const data = await res.json().catch(() => null) - const detail = data?.detail || res.statusText || 'Request failed' - throw new Error(detail) + throw new Error(data?.detail || res.statusText || 'Request failed') } const reader = res.body?.getReader() @@ -66,7 +73,6 @@ async function streamChatCompletion(opts: { if (done) break buffer += decoder.decode(value, { stream: true }) - // SSE frames separated by blank line const parts = buffer.split('\n\n') buffer = parts.pop() || '' for (const p of parts) { @@ -92,6 +98,99 @@ function getTier(subscription: any): 'scout' | 'trader' | 'tycoon' { return 'scout' } +// Simple markdown-like formatting to clean HTML +function formatMessage(text: string): string { + if (!text) return '' + let html = text + // Escape HTML + .replace(/&/g, '&') + .replace(//g, '>') + // Bold: **text** or __text__ + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/__(.+?)__/g, '$1') + // Italic: *text* or _text_ (but not inside words) + .replace(/(?$1') + .replace(/(?$1') + // Inline code: `code` + .replace(/`([^`]+)`/g, '$1') + // Line breaks + .replace(/\n/g, '
') + // Bullet points: - item or • item + .replace(/
[-•]\s+/g, '
• ') + return html +} + +// Suggestion chips based on current page +function getSuggestions(path: string): Array<{ label: string; prompt: string }> { + const p = (path || '').split('?')[0] + + if (p.startsWith('/terminal/hunt')) { + return [ + { label: 'Ending soon', prompt: 'Show auctions ending in the next 2 hours' }, + { label: 'Find brandables', prompt: 'Generate 10 available 5-letter brandable .com domains' }, + { label: 'Trending now', prompt: 'What keywords are trending today?' }, + ] + } + if (p.startsWith('/terminal/market')) { + return [ + { label: 'Best deals', prompt: 'Show the best deals under $100' }, + { label: 'Ending today', prompt: 'What auctions are ending today?' }, + ] + } + if (p.startsWith('/terminal/watchlist')) { + return [ + { label: 'My watchlist', prompt: 'Show my watchlist' }, + { label: 'Analyze top', prompt: 'Analyze my most recently added domain' }, + ] + } + if (p.startsWith('/terminal/portfolio')) { + return [ + { label: 'Portfolio ROI', prompt: 'What is my overall portfolio ROI?' }, + { label: 'Renewals', prompt: 'Which domains are up for renewal soon?' }, + { label: 'Top performers', prompt: 'Show my top 5 domains by value' }, + ] + } + if (p.startsWith('/terminal/sniper')) { + return [ + { label: 'My alerts', prompt: 'Show my sniper alerts' }, + { label: 'Matching now', prompt: 'Are there auctions matching my alerts right now?' }, + ] + } + if (p.startsWith('/terminal/listing')) { + return [ + { label: 'My listings', prompt: 'Show my active listings' }, + { label: 'New leads', prompt: 'Do I have any new leads?' }, + ] + } + if (p.startsWith('/terminal/inbox')) { + return [ + { label: 'Unread count', prompt: 'How many unread messages do I have?' }, + { label: 'Recent threads', prompt: 'Show my recent inquiry threads' }, + ] + } + if (p.startsWith('/terminal/yield')) { + return [ + { label: 'Yield stats', prompt: 'Show my yield dashboard stats' }, + { label: 'Top earners', prompt: 'Which domains are earning the most?' }, + ] + } + // Default + return [ + { label: 'Market snapshot', prompt: 'Give me a quick market snapshot' }, + { label: 'Analyze domain', prompt: 'Analyze ' }, + ] +} + +// Teaser examples for Scout +const TEASER_EXAMPLES = [ + { icon: Search, text: '"Analyze startup.io — should I buy?"' }, + { icon: TrendingUp, text: '"What auctions are ending in 2 hours?"' }, + { icon: Briefcase, text: '"What\'s my portfolio ROI?"' }, + { icon: Target, text: '"Find 5-letter .com brandables"' }, + { icon: Zap, text: '"Any new leads on my listings?"' }, +] + export function HunterCompanion() { const pathname = usePathname() const { subscription, user } = useStore() @@ -100,132 +199,94 @@ export function HunterCompanion() { const storageKey = useMemo(() => { const uidPart = user?.id ? String(user.id) : 'anon' - return `pounce:hunter_companion:v1:${uidPart}` + return `pounce:hunter_companion:v2:${uidPart}` }, [user?.id]) const [open, setOpen] = useState(false) const [input, setInput] = useState('') const [messages, setMessages] = useState([]) const [sending, setSending] = useState(false) - const [error, setError] = useState(null) const listRef = useRef(null) const inputRef = useRef(null) + // Load from localStorage useEffect(() => { + if (!canChat) return try { const raw = localStorage.getItem(storageKey) - if (!raw) return - const parsed = JSON.parse(raw) - if (Array.isArray(parsed)) setMessages(parsed) + if (raw) { + const parsed = JSON.parse(raw) + if (Array.isArray(parsed)) setMessages(parsed) + } } catch { // ignore } - }, [storageKey]) + }, [storageKey, canChat]) + // Save to localStorage useEffect(() => { + if (!canChat) return try { localStorage.setItem(storageKey, JSON.stringify(messages.slice(-60))) } catch { // ignore } - }, [messages, storageKey]) + }, [messages, storageKey, canChat]) + // Auto-scroll useEffect(() => { if (!open) return const el = listRef.current - if (!el) return - el.scrollTop = el.scrollHeight + if (el) el.scrollTop = el.scrollHeight }, [open, messages.length]) + // Focus input useEffect(() => { - if (!open) return - // Focus input on open - setTimeout(() => inputRef.current?.focus(), 0) - }, [open]) + if (open && canChat) { + setTimeout(() => inputRef.current?.focus(), 50) + } + }, [open, canChat]) - // Only show inside terminal routes + // Only show in terminal if (!pathname?.startsWith('/terminal')) return null - const send = async () => { - const text = input.trim() - if (!text || sending) return - setError(null) + const send = async (text?: string) => { + const msg = (text || input).trim() + if (!msg || sending || !canChat) return - // 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() }]) + const userMsg: ChatMessage = { id: uid(), role: 'user', content: msg, createdAt: Date.now() } + const assistantId = uid() + setMessages((prev) => [...prev, userMsg, { id: assistantId, role: 'assistant', content: '', createdAt: Date.now() }]) setInput('') setSending(true) - const system = [ - { - role: 'system' as const, - content: - '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: `Context: current_terminal_path=${pathname}; tier=${tier}.`, - }, - ] - - const history = messages - .slice(-20) - .map((m) => ({ role: m.role as Role, content: m.content })) + const history = messages.slice(-20).map((m) => ({ role: m.role as Role, content: m.content })) try { - await streamChatCompletion({ - messages: [...system, ...history, { role: 'user', content: text }], - temperature: 0.5, + await streamChat({ + messages: [...history, { role: 'user', content: msg }], path: pathname || '/terminal/hunt', onDelta: (delta) => { setMessages((prev) => - prev.map((m) => (m.id === assistantMsgId ? { ...m, content: (m.content || '') + delta } : m)) + prev.map((m) => (m.id === assistantId ? { ...m, content: (m.content || '') + delta } : m)) ) }, }) } catch (e: any) { - // If Scout: backend returns 403 -> use as upsell (real flow) - const msg = e?.message || 'Failed' - setError(msg) setMessages((prev) => prev.map((m) => - m.id === assistantMsgId - ? { - ...m, - content: canChat - ? `Request failed: ${msg}` - : `Hunter Companion is available on Trader & Tycoon. Upgrade to unlock live guidance.`, - } - : m + m.id === assistantId ? { ...m, content: `Error: ${e?.message || 'Request failed'}` } : m ) ) } finally { setSending(false) - // Keep input focused after sending so the user can continue typing immediately setTimeout(() => inputRef.current?.focus(), 0) } } const clear = () => { setMessages([]) - setError(null) try { localStorage.removeItem(storageKey) } catch { @@ -233,19 +294,22 @@ export function HunterCompanion() { } } + const suggestions = getSuggestions(pathname || '/terminal/hunt') + return ( <> {/* FAB */} @@ -254,34 +318,42 @@ export function HunterCompanion() {
setOpen(false)} /> -
+
{/* Header */} -
+
-
- Hunter Companion + {canChat ? ( +
+ ) : ( + + )} + + Hunter Companion + {!canChat && ( - - Scout + + LOCKED )}
-
- Tactical help for BUY / CONSIDER / SKIP +
+ {canChat ? 'Domain trading AI assistant' : 'Upgrade to unlock'}
-
- +
+ {canChat && messages.length > 0 && ( + + )}
{/* Body */} -
- {messages.length === 0 && ( -
-
- -
Start a hunt
+
+ {!canChat ? ( + /* Scout Teaser */ +
+
+
+ +
+

AI Trading Assistant

+

+ Get instant domain analysis, market insights, portfolio advice, and deal recommendations. +

+ +
+ {TEASER_EXAMPLES.map((ex, i) => ( +
+ + {ex.text} +
+ ))} +
+ + + Upgrade to Trader + +

+ Available on Trader & Tycoon plans +

-
- Paste a domain (or a short list) + your budget and I’ll give you a decisive BUY/CONSIDER/SKIP with reasoning. +
+ ) : messages.length === 0 ? ( + /* Empty state for Trader/Tycoon */ +
+
+
- {!canChat && ( -
-
- Trader & Tycoon get live guidance. -
- Ready to hunt +

+ Ask me anything about domains, auctions, or your portfolio. +

+ + {/* Quick suggestions */} +
+ {suggestions.map((s, i) => ( + + ))} +
+
+ ) : ( + /* Chat messages */ +
+ {messages.map((m) => ( +
+
+ {m.role === 'assistant' ? ( +
+ ) : ( + {m.content} + )} +
+
+ ))} + + {/* Suggestion chips after messages */} + {!sending && messages.length > 0 && ( +
+ {suggestions.slice(0, 2).map((s, i) => ( + + ))}
)}
)} +
- {messages.map((m) => ( -
-
+
+ setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + send() + } + }} + placeholder="Ask about domains, auctions, portfolio…" + className="flex-1 px-3 py-2.5 bg-white/[0.03] border border-white/[0.08] text-white text-xs font-mono outline-none focus:border-accent/30 placeholder:text-white/25" + disabled={sending} + /> +
+ {sending ? : } +
- ))} - - {error && ( -
- {error} -
- )} -
- - {/* Input */} -
-
- setInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - send() - } - }} - placeholder={canChat ? 'Ask Hunter Companion…' : 'Ask (will prompt upgrade)…'} - className="flex-1 px-3 py-2 bg-white/[0.03] border border-white/[0.08] text-white text-xs font-mono outline-none focus:border-accent/40" - disabled={sending} - /> -
-
- Trader/Tycoon: live AI. Scout: visible + upgrade prompt. - ESC closes -
-
+ )}
)} + + {/* Styles for formatted messages */} + ) } - - diff --git a/frontend/src/components/chat/terminalHelp.ts b/frontend/src/components/chat/terminalHelp.ts deleted file mode 100644 index edac55e..0000000 --- a/frontend/src/components/chat/terminalHelp.ts +++ /dev/null @@ -1,253 +0,0 @@ -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': { - overview: [ - 'This is the Terminal home. It routes you into the active modules (Hunt, Market, Watchlist, Portfolio, etc.).', - 'Use the sidebar (desktop) or bottom navigation (mobile) to move between modules.', - ], - sections: [ - { - title: 'Quick start', - bullets: [ - 'Hunt: discover opportunities (auctions, drops, trends, brandables).', - 'Watchlist: track targets and availability.', - 'Portfolio: manage owned domains (renewals, ROI).', - 'For Sale: list domains and manage leads.', - ], - }, - ], - }, - '/terminal/welcome': { - overview: [ - 'Welcome is the onboarding entry inside the Terminal.', - 'It’s designed to orient you and push you into your first workflow.', - ], - sections: [ - { - title: 'What you can do next', - bullets: [ - 'Go to Hunt to find opportunities.', - 'Add a domain to Watchlist to start tracking.', - 'Open Portfolio to model renewals and risk.', - ], - }, - ], - }, - '/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' - if (pathname === '/terminal/page' || pathname === '/terminal/page.tsx') return '/terminal' - return pathname -} - -function genericTerminalHelp(): HelpDoc { - return { - overview: [ - 'I can explain what this screen does, where to click, and the recommended workflow.', - 'Tell me your goal (e.g. “find brandables under $100” or “sell a domain”) and I’ll guide you step-by-step.', - ], - sections: [ - { - title: 'Core modules', - bullets: [ - 'Hunt: discovery engines (auctions, drops, search, trends, forge).', - 'Market: browse opportunities with filters.', - 'Watchlist: track targets and availability.', - 'Portfolio: manage owned domains (renewals, ROI).', - 'Sniper: set automated alerts.', - 'For Sale: list domains + handle leads.', - 'Inbox: buyer/seller threads.', - ], - }, - ], - } -} - -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] || genericTerminalHelp() - - 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() -} - -