Skip to content

Commit 036ae7d

Browse files
bobleerbowen628
andauthored
feat(flow-chat): pixel pet on chat input and session config (#340)
- Add ChatInputPixelPet with mood logic and unit tests - Extend AI experience / session config (web + core types) - i18n for session settings (en-US, zh-CN) Co-authored-by: bowen628 <bowen628@noreply.gitcode.com>
1 parent 8481015 commit 036ae7d

File tree

14 files changed

+1108
-12
lines changed

14 files changed

+1108
-12
lines changed

src/crates/core/src/service/config/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ pub struct AIExperienceConfig {
104104
pub enable_welcome_panel_ai_analysis: bool,
105105
/// Whether to enable visual mode.
106106
pub enable_visual_mode: bool,
107+
/// Whether to show the pixel Agent companion in the collapsed chat input.
108+
pub enable_agent_companion: bool,
107109
}
108110

109111
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -1022,6 +1024,7 @@ impl Default for AIExperienceConfig {
10221024
enable_session_title_generation: true,
10231025
enable_welcome_panel_ai_analysis: false,
10241026
enable_visual_mode: false,
1027+
enable_agent_companion: false,
10251028
}
10261029
}
10271030
}

src/web-ui/src/app/scenes/settings/settingsConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [
9292
'timeout',
9393
'confirmation',
9494
'history',
95+
'companion',
96+
'agent',
97+
'partner',
98+
'伙伴',
9599
],
96100
},
97101
{

src/web-ui/src/flow_chat/components/ChatInput.scss

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
transform: translateY(3px);
5555

5656
.bitfun-chat-input__box {
57+
position: relative;
58+
overflow: hidden;
5759
min-height: 42px;
5860
max-height: 42px;
5961
padding: 0 18px;
@@ -220,6 +222,96 @@
220222
&:hover .bitfun-chat-input__space-hint {
221223
opacity: 0.9;
222224
}
225+
226+
&.bitfun-chat-input--pet-visible {
227+
.bitfun-chat-input__input-area {
228+
z-index: 1;
229+
}
230+
231+
.bitfun-chat-input__actions {
232+
z-index: 2;
233+
}
234+
235+
.bitfun-chat-input__space-hint {
236+
opacity: 0;
237+
transition: opacity 0.36s cubic-bezier(0.4, 0, 0.2, 1);
238+
}
239+
240+
&:hover .bitfun-chat-input__space-hint {
241+
opacity: 0.88;
242+
}
243+
244+
&.bitfun-chat-input--pet-split-send.bitfun-chat-input--processing {
245+
.bitfun-chat-input__actions {
246+
z-index: 5;
247+
}
248+
}
249+
}
250+
251+
&.bitfun-chat-input--pet-replaces-stop.bitfun-chat-input--processing {
252+
.bitfun-chat-input__input-area {
253+
right: 52px;
254+
}
255+
256+
&.bitfun-chat-input--pet-split-send .bitfun-chat-input__input-area {
257+
right: 112px;
258+
}
259+
}
260+
261+
.bitfun-chat-input__pet-wrap {
262+
position: absolute;
263+
inset: 0;
264+
z-index: 3;
265+
pointer-events: none;
266+
}
267+
268+
/* Full width so flex alignment is relative to the capsule (not the ~40px pet). */
269+
.bitfun-chat-input__pet-inner {
270+
box-sizing: border-box;
271+
width: 100%;
272+
height: 100%;
273+
display: flex;
274+
align-items: center;
275+
justify-content: center;
276+
padding-left: 0;
277+
padding-right: 0;
278+
transition:
279+
justify-content 0.45s cubic-bezier(0.34, 1.15, 0.64, 1),
280+
padding 0.45s cubic-bezier(0.34, 1.15, 0.64, 1);
281+
}
282+
283+
/* Align with former stop control (~same inset as .bitfun-chat-input__actions { right: 14px }). */
284+
.bitfun-chat-input__pet-wrap--shift .bitfun-chat-input__pet-inner {
285+
justify-content: flex-end;
286+
padding-right: 8px;
287+
}
288+
289+
.bitfun-chat-input__pet-wrap--shift.bitfun-chat-input__pet-wrap--split .bitfun-chat-input__pet-inner {
290+
padding-right: 42px;
291+
}
292+
293+
.bitfun-chat-input__pet-stop-btn {
294+
pointer-events: auto;
295+
display: flex;
296+
align-items: center;
297+
justify-content: center;
298+
padding: 0;
299+
margin: 0;
300+
border: none;
301+
background: transparent;
302+
cursor: pointer;
303+
border-radius: 10px;
304+
line-height: 0;
305+
306+
&:hover {
307+
filter: brightness(1.08);
308+
}
309+
310+
&:focus-visible {
311+
outline: 2px solid var(--color-accent-primary);
312+
outline-offset: 2px;
313+
}
314+
}
223315
}
224316

225317
&__space-hint {

src/web-ui/src/flow_chat/components/ChatInput.tsx

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { useActiveSessionState } from '../hooks/useActiveSessionState';
1111
import { RichTextInput, type MentionState } from './RichTextInput';
1212
import { FileMentionPicker } from './FileMentionPicker';
1313
import { globalEventBus } from '../../infrastructure/event-bus';
14-
import { useSessionDerivedState, useSessionStateMachineActions } from '../hooks/useSessionStateMachine';
14+
import {
15+
useSessionDerivedState,
16+
useSessionStateMachine,
17+
useSessionStateMachineActions,
18+
} from '../hooks/useSessionStateMachine';
1519
import { SessionExecutionEvent } from '../state-machine/types';
1620
import TokenUsageIndicator from './TokenUsageIndicator';
1721
import { ModelSelector } from './ModelSelector';
@@ -39,6 +43,9 @@ import { resolveSessionRelationship } from '../utils/sessionMetadata';
3943
import { useSceneStore } from '@/app/stores/sceneStore';
4044
import type { SceneTabId } from '@/app/components/SceneBar/types';
4145
import type { SkillInfo } from '@/infrastructure/config/types';
46+
import { aiExperienceConfigService } from '@/infrastructure/config/services/AIExperienceConfigService';
47+
import { deriveChatInputPetMood } from '../utils/chatInputPetMood';
48+
import { ChatInputPixelPet } from './ChatInputPixelPet';
4249
import './ChatInput.scss';
4350

4451
const log = createLogger('ChatInput');
@@ -136,6 +143,22 @@ export const ChatInput: React.FC<ChatInputProps> = ({
136143
effectiveTargetSessionId,
137144
inputState.value.trim()
138145
);
146+
const sessionMachineSnapshot = useSessionStateMachine(effectiveTargetSessionId);
147+
const petMood = useMemo(
148+
() => deriveChatInputPetMood(sessionMachineSnapshot),
149+
[sessionMachineSnapshot],
150+
);
151+
const [agentCompanionEnabled, setAgentCompanionEnabled] = useState(
152+
() => aiExperienceConfigService.getSettings().enable_agent_companion,
153+
);
154+
useEffect(() => {
155+
setAgentCompanionEnabled(aiExperienceConfigService.getSettings().enable_agent_companion);
156+
return aiExperienceConfigService.addChangeListener(settings => {
157+
setAgentCompanionEnabled(settings.enable_agent_companion);
158+
});
159+
}, []);
160+
const showCollapsedPet =
161+
agentCompanionEnabled && !inputState.isActive && !inputState.value.trim();
139162
const { transition, setQueuedInput } = useSessionStateMachineActions(effectiveTargetSessionId);
140163

141164
const { workspace, workspacePath } = useCurrentWorkspace();
@@ -1729,10 +1752,37 @@ export const ChatInput: React.FC<ChatInputProps> = ({
17291752
return () => observer.disconnect();
17301753
// eslint-disable-next-line react-hooks/exhaustive-deps
17311754
}, []);
1732-
1755+
1756+
const isCollapsedProcessing = !inputState.isActive && !!derivedState?.isProcessing;
1757+
const petReplacesStopChrome = agentCompanionEnabled && isCollapsedProcessing;
1758+
const petStopClickable = petReplacesStopChrome && !!derivedState?.canCancel;
1759+
const collapsedPetSplitSend =
1760+
petReplacesStopChrome && derivedState?.sendButtonMode === 'split';
1761+
17331762
const renderActionButton = () => {
17341763
if (!derivedState) return <IconButton className="bitfun-chat-input__send-button" disabled size="small"><ArrowUp size={11} /></IconButton>;
1735-
1764+
1765+
if (petReplacesStopChrome) {
1766+
const { sendButtonMode } = derivedState;
1767+
if (sendButtonMode === 'cancel') {
1768+
return null;
1769+
}
1770+
if (sendButtonMode === 'split') {
1771+
return (
1772+
<IconButton
1773+
className="bitfun-chat-input__send-button"
1774+
onClick={handleSendOrCancel}
1775+
disabled={!inputState.value.trim()}
1776+
data-testid="chat-input-send-btn"
1777+
tooltip={t('input.sendShortcut')}
1778+
size="small"
1779+
>
1780+
<ArrowUp size={11} />
1781+
</IconButton>
1782+
);
1783+
}
1784+
}
1785+
17361786
const { sendButtonMode, hasQueuedInput } = derivedState;
17371787

17381788
if (sendButtonMode === 'cancel') {
@@ -1805,8 +1855,6 @@ export const ChatInput: React.FC<ChatInputProps> = ({
18051855
);
18061856
};
18071857

1808-
const isCollapsedProcessing = !inputState.isActive && !!derivedState?.isProcessing;
1809-
18101858
return (
18111859
<>
18121860
<ContextDropZone
@@ -1827,7 +1875,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
18271875
>
18281876
<div
18291877
ref={containerRef}
1830-
className={`bitfun-chat-input ${inputState.isActive ? 'bitfun-chat-input--active' : 'bitfun-chat-input--collapsed'} ${inputState.isExpanded ? 'bitfun-chat-input--expanded' : ''} ${derivedState?.isProcessing ? 'bitfun-chat-input--processing' : ''} ${className}`}
1878+
className={`bitfun-chat-input ${inputState.isActive ? 'bitfun-chat-input--active' : 'bitfun-chat-input--collapsed'} ${inputState.isExpanded ? 'bitfun-chat-input--expanded' : ''} ${derivedState?.isProcessing ? 'bitfun-chat-input--processing' : ''} ${showCollapsedPet ? 'bitfun-chat-input--pet-visible' : ''} ${petReplacesStopChrome ? 'bitfun-chat-input--pet-replaces-stop' : ''} ${collapsedPetSplitSend ? 'bitfun-chat-input--pet-split-send' : ''} ${className}`}
18311879
onClick={!inputState.isActive ? handleActivate : undefined}
18321880
data-testid="chat-input-container"
18331881
>
@@ -1840,6 +1888,41 @@ export const ChatInput: React.FC<ChatInputProps> = ({
18401888

18411889
<div className="bitfun-chat-input__container">
18421890
<div className={`bitfun-chat-input__box ${inputState.isExpanded ? 'bitfun-chat-input__box--expanded' : ''}`}>
1891+
{showCollapsedPet && (
1892+
<div
1893+
className={[
1894+
'bitfun-chat-input__pet-wrap',
1895+
petReplacesStopChrome ? 'bitfun-chat-input__pet-wrap--shift' : '',
1896+
collapsedPetSplitSend ? 'bitfun-chat-input__pet-wrap--split' : '',
1897+
]
1898+
.filter(Boolean)
1899+
.join(' ')}
1900+
>
1901+
<div className="bitfun-chat-input__pet-inner">
1902+
{petStopClickable ? (
1903+
<button
1904+
type="button"
1905+
className="bitfun-chat-input__pet-stop-btn"
1906+
onClick={e => {
1907+
e.stopPropagation();
1908+
void transition(SessionExecutionEvent.USER_CANCEL);
1909+
}}
1910+
aria-label={t('input.stopGeneration')}
1911+
>
1912+
<ChatInputPixelPet
1913+
mood={petMood}
1914+
layout={petReplacesStopChrome ? 'stopRight' : 'center'}
1915+
/>
1916+
</button>
1917+
) : (
1918+
<ChatInputPixelPet
1919+
mood={petMood}
1920+
layout={petReplacesStopChrome ? 'stopRight' : 'center'}
1921+
/>
1922+
)}
1923+
</div>
1924+
</div>
1925+
)}
18431926
{showTargetSwitcher && (
18441927
<div className="bitfun-chat-input__target-switcher" data-testid="chat-input-target-switcher">
18451928
<span className="bitfun-chat-input__target-switcher-label">{t('chatInput.conversationTarget')}</span>
@@ -1884,7 +1967,10 @@ export const ChatInput: React.FC<ChatInputProps> = ({
18841967
data-testid="chat-input-textarea"
18851968
/>
18861969

1887-
{!inputState.isActive && !inputState.value.trim() && (
1970+
{!inputState.isActive &&
1971+
!inputState.value.trim() &&
1972+
!agentCompanionEnabled &&
1973+
!isCollapsedProcessing && (
18881974
<span className="bitfun-chat-input__space-hint">
18891975
<Trans
18901976
i18nKey="input.spaceToActivate"
@@ -2239,7 +2325,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
22392325
)}
22402326
</div>
22412327
<div className="bitfun-chat-input__actions-right">
2242-
{isCollapsedProcessing && (
2328+
{isCollapsedProcessing && !petReplacesStopChrome && (
22432329
<>
22442330
<span className="bitfun-chat-input__capsule-divider" />
22452331
<span className="bitfun-chat-input__cancel-shortcut">

0 commit comments

Comments
 (0)