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
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:
@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { MessageSquare, X, Send, Sparkles, Loader2, Lock, Trash2 } from 'lucide-react'
|
import { MessageSquare, X, Send, Sparkles, Loader2, Lock, Trash2 } from 'lucide-react'
|
||||||
import { useStore } from '@/lib/store'
|
import { useStore } from '@/lib/store'
|
||||||
|
import { buildUiHelpAnswer, isUiHelpQuestion } from '@/components/chat/terminalHelp'
|
||||||
|
|
||||||
type Role = 'system' | 'user' | 'assistant'
|
type Role = 'system' | 'user' | 'assistant'
|
||||||
|
|
||||||
@ -106,6 +107,7 @@ export function HunterCompanion() {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const listRef = useRef<HTMLDivElement | null>(null)
|
const listRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
@ -133,6 +135,12 @@ export function HunterCompanion() {
|
|||||||
el.scrollTop = el.scrollHeight
|
el.scrollTop = el.scrollHeight
|
||||||
}, [open, messages.length])
|
}, [open, messages.length])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
// Focus input on open
|
||||||
|
setTimeout(() => inputRef.current?.focus(), 0)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
// Only show inside terminal routes
|
// Only show inside terminal routes
|
||||||
if (!pathname?.startsWith('/terminal')) return null
|
if (!pathname?.startsWith('/terminal')) return null
|
||||||
|
|
||||||
@ -141,6 +149,21 @@ export function HunterCompanion() {
|
|||||||
if (!text || sending) return
|
if (!text || sending) return
|
||||||
setError(null)
|
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 userMsg: ChatMessage = { id: uid(), role: 'user', content: text, createdAt: Date.now() }
|
||||||
const assistantMsgId = uid()
|
const assistantMsgId = uid()
|
||||||
setMessages((prev) => [...prev, userMsg, { id: assistantMsgId, role: 'assistant', content: '', createdAt: Date.now() }])
|
setMessages((prev) => [...prev, userMsg, { id: assistantMsgId, role: 'assistant', content: '', createdAt: Date.now() }])
|
||||||
@ -151,11 +174,11 @@ export function HunterCompanion() {
|
|||||||
{
|
{
|
||||||
role: 'system' as const,
|
role: 'system' as const,
|
||||||
content:
|
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,
|
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 {
|
try {
|
||||||
await streamChatCompletion({
|
await streamChatCompletion({
|
||||||
messages: [...system, ...history, { role: 'user', content: text }],
|
messages: [...system, ...history, { role: 'user', content: text }],
|
||||||
temperature: 0.6,
|
temperature: 0.7,
|
||||||
onDelta: (delta) => {
|
onDelta: (delta) => {
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((m) => (m.id === assistantMsgId ? { ...m, content: (m.content || '') + delta } : m))
|
prev.map((m) => (m.id === assistantMsgId ? { ...m, content: (m.content || '') + delta } : m))
|
||||||
@ -191,6 +214,8 @@ export function HunterCompanion() {
|
|||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false)
|
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="p-3 border-t border-white/[0.08] bg-[#050505]">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
|||||||
196
frontend/src/components/chat/terminalHelp.ts
Normal file
196
frontend/src/components/chat/terminalHelp.ts
Normal 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.',
|
||||||
|
'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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user