Hunter Companion: English-only expert + keep input focus + Scout UI-help mode
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-17 14:00:21 +01:00
parent fcd36a0a29
commit b8afdc812f
2 changed files with 225 additions and 3 deletions

View File

@ -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<string | null>(null)
const listRef = useRef<HTMLDivElement | null>(null)
const inputRef = useRef<HTMLInputElement | null>(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 Domainbezogen ist, antworte zusätzlich strukturiert und entscheidungsorientiert:\n- Empfehlung: BUY / CONSIDER / SKIP\n- 36 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 users question is domain-related, be decisional and structured:\n- Recommendation: BUY / CONSIDER / SKIP\n- 36 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() {
<div className="p-3 border-t border-white/[0.08] bg-[#050505]">
<div className="flex items-center gap-2">
<input
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {

View File

@ -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<string, HelpDoc> = {
'/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.',
'Its 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: its 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("Heres 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”), Ill give you a step-by-step flow.')
return lines.join('\n').trim()
}