Spaces:
Sleeping
Sleeping
File size: 5,235 Bytes
d0878a6 ffcad5d d0878a6 ffcad5d d0878a6 ffcad5d d0878a6 ffcad5d d0878a6 ffcad5d d0878a6 ffcad5d d0878a6 ffcad5d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import React, { useEffect, useMemo, useState } from 'react';
import { useAppConfig } from '../contexts/AppConfigContext';
import SecurityChatBotIcon from './icons/SecurityChatBotIcon';
import { goalCacheKey } from '../utils/statedGoal';
import { loadStarterGreeting, saveStarterGreeting } from '../utils/starterCache';
import '../styles/IntakePanel.css';
const STATIC_GREETING = 'What cybersecurity problem should we work on first?';
const STATIC_SUBHEADER =
'I can help with threats, controls, incidents, compliance, or your security career.';
/**
* First-session intake: goal-specific greeting + chips + optional free-text.
* Chips come from /api/config → chat_page.intake, filtered by guest persona when set.
*/
const IntakePanel = ({
onSubmit,
guestPersona = null,
authToken = null,
user = null,
statedGoal = '',
}) => {
const { config } = useAppConfig();
const intake = config?.chat_page?.intake || {};
const yamlGreeting = intake.greeting || STATIC_GREETING;
const yamlSubheader = intake.greeting_subheader || STATIC_SUBHEADER;
const cacheKey = useMemo(
() => goalCacheKey(statedGoal, user?.id || user?._id || user?.email || (user?.is_guest ? 'guest' : '')),
[statedGoal, user],
);
const cached = useMemo(() => loadStarterGreeting(cacheKey), [cacheKey]);
const [greeting, setGreeting] = useState(cached?.greeting || yamlGreeting);
const [subheader, setSubheader] = useState(cached?.subheader || yamlSubheader);
useEffect(() => {
const stored = loadStarterGreeting(cacheKey);
if (stored?.greeting) {
setGreeting(stored.greeting);
setSubheader(stored.subheader || yamlSubheader);
return undefined;
}
if (!authToken) {
setGreeting(yamlGreeting);
setSubheader(yamlSubheader);
return undefined;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
(async () => {
try {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/chat/starter-greeting`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
signal: controller.signal,
},
);
if (!response.ok) return;
const data = await response.json();
if (!data?.greeting) return;
setGreeting(data.greeting);
if (data.subheader) setSubheader(data.subheader);
saveStarterGreeting(cacheKey, {
greeting: data.greeting,
subheader: data.subheader || yamlSubheader,
});
} catch {
/* keep static fallback */
}
})();
return () => {
clearTimeout(timeout);
controller.abort();
};
}, [authToken, cacheKey, yamlGreeting, yamlSubheader]);
const chips = useMemo(() => {
const byPersona = intake.by_persona || {};
const personaKey = (guestPersona || '').toLowerCase();
const personaChips = personaKey && Array.isArray(byPersona[personaKey])
? byPersona[personaKey]
: null;
if (personaChips && personaChips.length > 0) return personaChips;
return Array.isArray(intake.chips) ? intake.chips : [];
}, [intake, guestPersona]);
const [freeText, setFreeText] = useState('');
const [showFreeText, setShowFreeText] = useState(false);
const handleChip = (chip) => {
if (chip.free_text) {
setShowFreeText(true);
return;
}
if (chip.prompt) onSubmit(chip.prompt);
};
const handleFreeSubmit = (e) => {
e.preventDefault();
const text = freeText.trim();
if (!text) return;
onSubmit(text);
setFreeText('');
};
return (
<div className="intake-panel" role="region" aria-label="Getting started">
<div className="intake-avatar" aria-hidden="true">
<SecurityChatBotIcon size={22} />
</div>
<h2 className="intake-greeting">{greeting}</h2>
{subheader ? <p className="intake-sub">{subheader}</p> : null}
<div className="intake-chips">
{chips.map((chip) => (
<button
key={chip.id}
type="button"
className={`intake-chip${chip.free_text ? ' intake-chip--other' : ''}`}
onClick={() => handleChip(chip)}
>
{chip.label}
</button>
))}
</div>
{(showFreeText || chips.length === 0) && (
<form className="intake-freetext" onSubmit={handleFreeSubmit}>
<label htmlFor="intake-free" className="sr-only">
Describe your cybersecurity need
</label>
<textarea
id="intake-free"
rows={3}
value={freeText}
onChange={(e) => setFreeText(e.target.value)}
placeholder="Tell me about the cybersecurity problem or goal you have today…"
/>
<button type="submit" className="intake-submit" disabled={!freeText.trim()}>
Start
</button>
</form>
)}
</div>
);
};
export default IntakePanel;
|