@haro/pochidesk-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ChatBridge.d.ts +3 -0
- package/dist/ChatBridge.js +94 -0
- package/dist/PochiDesk.d.ts +3 -0
- package/dist/PochiDesk.js +83 -0
- package/dist/assets/icons.d.ts +6 -0
- package/dist/assets/icons.js +20 -0
- package/dist/components/ChatPanel.d.ts +15 -0
- package/dist/components/ChatPanel.js +43 -0
- package/dist/components/MessageInput.d.ts +9 -0
- package/dist/components/MessageInput.js +64 -0
- package/dist/components/MessageList.d.ts +10 -0
- package/dist/components/MessageList.js +61 -0
- package/dist/components/ToolConfirmDialog.d.ts +9 -0
- package/dist/components/ToolConfirmDialog.js +44 -0
- package/dist/hooks/useChatMessages.d.ts +9 -0
- package/dist/hooks/useChatMessages.js +50 -0
- package/dist/hooks/useChatSession.d.ts +2 -0
- package/dist/hooks/useChatSession.js +138 -0
- package/dist/hooks/useStreaming.d.ts +2 -0
- package/dist/hooks/useStreaming.js +111 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/lib/sse-client.d.ts +2 -0
- package/dist/lib/sse-client.js +55 -0
- package/dist/presets/index.d.ts +22 -0
- package/dist/presets/index.js +50 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { useChatSession } from './hooks/useChatSession';
|
|
4
|
+
import { useStreaming } from './hooks/useStreaming';
|
|
5
|
+
import { useChatMessages } from './hooks/useChatMessages';
|
|
6
|
+
import { ChatPanel } from './components/ChatPanel';
|
|
7
|
+
const DEFAULT_THEME = {
|
|
8
|
+
primaryColor: '#6366f1',
|
|
9
|
+
textColor: '#1f2937',
|
|
10
|
+
backgroundColor: '#ffffff',
|
|
11
|
+
userBubbleColor: '#6366f1',
|
|
12
|
+
assistantBubbleColor: '#f3f4f6',
|
|
13
|
+
fontFamily: '"Noto Sans JP", "Helvetica Neue", Arial, sans-serif',
|
|
14
|
+
fontSize: 14,
|
|
15
|
+
borderRadius: 12,
|
|
16
|
+
panelWidth: 400,
|
|
17
|
+
panelHeight: 600,
|
|
18
|
+
};
|
|
19
|
+
export const ChatBridge = ({ botId, token, apiUrl = '', position = 'bottom-right', theme: themeOverride, onToolCall, onSessionCreate, onError, }) => {
|
|
20
|
+
const [isOpen, setIsOpen] = useState(position === 'inline');
|
|
21
|
+
const theme = useMemo(() => ({ ...DEFAULT_THEME, ...themeOverride }), [themeOverride]);
|
|
22
|
+
const sessionHook = useChatSession(botId, token, apiUrl);
|
|
23
|
+
const streamingHook = useStreaming(botId, token, apiUrl, sessionHook.session?.id ?? null, onToolCall);
|
|
24
|
+
const messagesHook = useChatMessages(botId, token, apiUrl, sessionHook.session?.id ?? null);
|
|
25
|
+
// Auto-create session on mount if none exists
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!sessionHook.session && !sessionHook.isLoading) {
|
|
28
|
+
sessionHook.createSession();
|
|
29
|
+
}
|
|
30
|
+
}, [sessionHook.session, sessionHook.isLoading]);
|
|
31
|
+
// Notify parent when session is created
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (sessionHook.session && onSessionCreate) {
|
|
34
|
+
onSessionCreate(sessionHook.session.id);
|
|
35
|
+
}
|
|
36
|
+
}, [sessionHook.session?.id]);
|
|
37
|
+
// Forward errors
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (sessionHook.error && onError) {
|
|
40
|
+
onError(sessionHook.error);
|
|
41
|
+
}
|
|
42
|
+
}, [sessionHook.error]);
|
|
43
|
+
// Merge history messages with streaming messages
|
|
44
|
+
const allMessages = useMemo(() => {
|
|
45
|
+
if (streamingHook.messages.length > 0) {
|
|
46
|
+
return streamingHook.messages;
|
|
47
|
+
}
|
|
48
|
+
return messagesHook.messages;
|
|
49
|
+
}, [messagesHook.messages, streamingHook.messages]);
|
|
50
|
+
// Handle Escape key
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (position === 'inline')
|
|
53
|
+
return;
|
|
54
|
+
const handleKeyDown = (e) => {
|
|
55
|
+
if (e.key === 'Escape' && isOpen) {
|
|
56
|
+
setIsOpen(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
60
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
61
|
+
}, [isOpen, position]);
|
|
62
|
+
// Inline mode: render panel directly
|
|
63
|
+
if (position === 'inline') {
|
|
64
|
+
return (_jsx("div", { style: { width: '100%', height: '100%' }, children: _jsx(ChatPanel, { messages: allMessages, isStreaming: streamingHook.isStreaming, onSend: streamingHook.sendMessage, theme: theme, pendingToolCall: streamingHook.pendingToolCall, onConfirmToolCall: streamingHook.confirmToolCall }) }));
|
|
65
|
+
}
|
|
66
|
+
// Floating mode
|
|
67
|
+
const isRight = position === 'bottom-right';
|
|
68
|
+
return (_jsxs(_Fragment, { children: [isOpen && (_jsx("div", { style: {
|
|
69
|
+
position: 'fixed',
|
|
70
|
+
bottom: '80px',
|
|
71
|
+
...(isRight ? { right: '20px' } : { left: '20px' }),
|
|
72
|
+
width: `${theme.panelWidth}px`,
|
|
73
|
+
height: `${theme.panelHeight}px`,
|
|
74
|
+
zIndex: 9999,
|
|
75
|
+
}, children: _jsx(ChatPanel, { messages: allMessages, isStreaming: streamingHook.isStreaming, onSend: streamingHook.sendMessage, onClose: () => setIsOpen(false), theme: theme, pendingToolCall: streamingHook.pendingToolCall, onConfirmToolCall: streamingHook.confirmToolCall }) })), _jsx("button", { type: "button", "aria-label": isOpen ? 'チャットを閉じる' : 'チャットを開く', onClick: () => setIsOpen((prev) => !prev), style: {
|
|
76
|
+
position: 'fixed',
|
|
77
|
+
bottom: '20px',
|
|
78
|
+
...(isRight ? { right: '20px' } : { left: '20px' }),
|
|
79
|
+
width: '56px',
|
|
80
|
+
height: '56px',
|
|
81
|
+
borderRadius: '50%',
|
|
82
|
+
border: 'none',
|
|
83
|
+
backgroundColor: theme.primaryColor,
|
|
84
|
+
color: '#ffffff',
|
|
85
|
+
cursor: 'pointer',
|
|
86
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
87
|
+
display: 'flex',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
justifyContent: 'center',
|
|
90
|
+
fontSize: '24px',
|
|
91
|
+
zIndex: 9999,
|
|
92
|
+
transition: 'transform 0.2s ease',
|
|
93
|
+
}, children: isOpen ? '\u2715' : '\u{1F4AC}' })] }));
|
|
94
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { useChatSession } from './hooks/useChatSession';
|
|
4
|
+
import { useStreaming } from './hooks/useStreaming';
|
|
5
|
+
import { useChatMessages } from './hooks/useChatMessages';
|
|
6
|
+
import { resolvePreset } from './presets';
|
|
7
|
+
import { CLOSE_SVG } from './assets/icons';
|
|
8
|
+
import { ChatPanel } from './components/ChatPanel';
|
|
9
|
+
export const PochiDesk = ({ botId, token, apiUrl = '', position = 'bottom-right', preset, theme: themeOverride, onToolCall, onSessionCreate, onError, }) => {
|
|
10
|
+
const [isOpen, setIsOpen] = useState(position === 'inline');
|
|
11
|
+
const { theme, preset: presetConfig } = useMemo(() => resolvePreset(preset, themeOverride), [preset, themeOverride]);
|
|
12
|
+
const sessionHook = useChatSession(botId, token, apiUrl);
|
|
13
|
+
const streamingHook = useStreaming(botId, token, apiUrl, sessionHook.session?.id ?? null, onToolCall);
|
|
14
|
+
const messagesHook = useChatMessages(botId, token, apiUrl, sessionHook.session?.id ?? null);
|
|
15
|
+
// Auto-create session on mount if none exists
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!sessionHook.session && !sessionHook.isLoading) {
|
|
18
|
+
sessionHook.createSession();
|
|
19
|
+
}
|
|
20
|
+
}, [sessionHook.session, sessionHook.isLoading]);
|
|
21
|
+
// Notify parent when session is created
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (sessionHook.session && onSessionCreate) {
|
|
24
|
+
onSessionCreate(sessionHook.session.id);
|
|
25
|
+
}
|
|
26
|
+
}, [sessionHook.session?.id]);
|
|
27
|
+
// Forward errors
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (sessionHook.error && onError) {
|
|
30
|
+
onError(sessionHook.error);
|
|
31
|
+
}
|
|
32
|
+
}, [sessionHook.error]);
|
|
33
|
+
// Merge history messages with streaming messages
|
|
34
|
+
const allMessages = useMemo(() => {
|
|
35
|
+
if (streamingHook.messages.length > 0) {
|
|
36
|
+
return streamingHook.messages;
|
|
37
|
+
}
|
|
38
|
+
return messagesHook.messages;
|
|
39
|
+
}, [messagesHook.messages, streamingHook.messages]);
|
|
40
|
+
// Handle Escape key
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (position === 'inline')
|
|
43
|
+
return;
|
|
44
|
+
const handleKeyDown = (e) => {
|
|
45
|
+
if (e.key === 'Escape' && isOpen) {
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
50
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
51
|
+
}, [isOpen, position]);
|
|
52
|
+
// Inline mode: render panel directly
|
|
53
|
+
if (position === 'inline') {
|
|
54
|
+
return (_jsx("div", { style: { width: '100%', height: '100%' }, children: _jsx(ChatPanel, { messages: allMessages, isStreaming: streamingHook.isStreaming, onSend: streamingHook.sendMessage, theme: theme, presetConfig: presetConfig, pendingToolCall: streamingHook.pendingToolCall, onConfirmToolCall: streamingHook.confirmToolCall }) }));
|
|
55
|
+
}
|
|
56
|
+
// Floating mode
|
|
57
|
+
const isRight = position === 'bottom-right';
|
|
58
|
+
return (_jsxs(_Fragment, { children: [isOpen && (_jsx("div", { style: {
|
|
59
|
+
position: 'fixed',
|
|
60
|
+
bottom: '80px',
|
|
61
|
+
...(isRight ? { right: '20px' } : { left: '20px' }),
|
|
62
|
+
width: `${theme.panelWidth}px`,
|
|
63
|
+
height: `${theme.panelHeight}px`,
|
|
64
|
+
zIndex: 9999,
|
|
65
|
+
}, children: _jsx(ChatPanel, { messages: allMessages, isStreaming: streamingHook.isStreaming, onSend: streamingHook.sendMessage, onClose: () => setIsOpen(false), theme: theme, presetConfig: presetConfig, pendingToolCall: streamingHook.pendingToolCall, onConfirmToolCall: streamingHook.confirmToolCall }) })), _jsx("button", { type: "button", "aria-label": isOpen ? 'チャットを閉じる' : 'チャットを開く', onClick: () => setIsOpen((prev) => !prev), style: {
|
|
66
|
+
position: 'fixed',
|
|
67
|
+
bottom: '20px',
|
|
68
|
+
...(isRight ? { right: '20px' } : { left: '20px' }),
|
|
69
|
+
width: '56px',
|
|
70
|
+
height: '56px',
|
|
71
|
+
borderRadius: '50%',
|
|
72
|
+
border: 'none',
|
|
73
|
+
backgroundColor: theme.primaryColor,
|
|
74
|
+
color: '#ffffff',
|
|
75
|
+
cursor: 'pointer',
|
|
76
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
77
|
+
display: 'flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
justifyContent: 'center',
|
|
80
|
+
zIndex: 9999,
|
|
81
|
+
transition: 'transform 0.2s ease',
|
|
82
|
+
}, children: _jsx("span", { style: { width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, dangerouslySetInnerHTML: { __html: isOpen ? CLOSE_SVG : presetConfig.fabIconSvg } }) })] }));
|
|
83
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Pochi (dog) face icon — friendly mascot for FAB, header, and avatar */
|
|
2
|
+
export declare const POCHI_DOG_SVG = "<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n <ellipse cx=\"12\" cy=\"13.5\" rx=\"7\" ry=\"6.5\" opacity=\"0.15\"/>\n <path d=\"M4.5 3C3.5 3 2.5 4 2.5 5.5C2.5 7 3.2 8.5 4.5 10C5.5 8 7.5 6.5 10 6C8 5 6 3.5 4.5 3Z\"/>\n <path d=\"M19.5 3C20.5 3 21.5 4 21.5 5.5C21.5 7 20.8 8.5 19.5 10C18.5 8 16.5 6.5 14 6C16 5 18 3.5 19.5 3Z\"/>\n <ellipse cx=\"12\" cy=\"14\" rx=\"6.5\" ry=\"6\"/>\n <ellipse cx=\"9.5\" cy=\"12.5\" rx=\"1\" ry=\"1.1\" fill=\"#fff\"/>\n <ellipse cx=\"14.5\" cy=\"12.5\" rx=\"1\" ry=\"1.1\" fill=\"#fff\"/>\n <ellipse cx=\"12\" cy=\"15\" rx=\"1.5\" ry=\"1\" opacity=\"0.3\"/>\n <circle cx=\"12\" cy=\"14.8\" r=\"0.6\" opacity=\"0.5\"/>\n <path d=\"M10.5 16.5Q12 17.5 13.5 16.5\" stroke=\"#fff\" stroke-width=\"0.5\" fill=\"none\" stroke-linecap=\"round\"/>\n</svg>";
|
|
3
|
+
/** Chat bubble icon — used for business preset FAB */
|
|
4
|
+
export declare const CHAT_BUBBLE_SVG = "<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n <path d=\"M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z\"/>\n</svg>";
|
|
5
|
+
/** Close (X) icon */
|
|
6
|
+
export declare const CLOSE_SVG = "<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/>\n</svg>";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Pochi (dog) face icon — friendly mascot for FAB, header, and avatar */
|
|
2
|
+
export const POCHI_DOG_SVG = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
|
3
|
+
<ellipse cx="12" cy="13.5" rx="7" ry="6.5" opacity="0.15"/>
|
|
4
|
+
<path d="M4.5 3C3.5 3 2.5 4 2.5 5.5C2.5 7 3.2 8.5 4.5 10C5.5 8 7.5 6.5 10 6C8 5 6 3.5 4.5 3Z"/>
|
|
5
|
+
<path d="M19.5 3C20.5 3 21.5 4 21.5 5.5C21.5 7 20.8 8.5 19.5 10C18.5 8 16.5 6.5 14 6C16 5 18 3.5 19.5 3Z"/>
|
|
6
|
+
<ellipse cx="12" cy="14" rx="6.5" ry="6"/>
|
|
7
|
+
<ellipse cx="9.5" cy="12.5" rx="1" ry="1.1" fill="#fff"/>
|
|
8
|
+
<ellipse cx="14.5" cy="12.5" rx="1" ry="1.1" fill="#fff"/>
|
|
9
|
+
<ellipse cx="12" cy="15" rx="1.5" ry="1" opacity="0.3"/>
|
|
10
|
+
<circle cx="12" cy="14.8" r="0.6" opacity="0.5"/>
|
|
11
|
+
<path d="M10.5 16.5Q12 17.5 13.5 16.5" stroke="#fff" stroke-width="0.5" fill="none" stroke-linecap="round"/>
|
|
12
|
+
</svg>`;
|
|
13
|
+
/** Chat bubble icon — used for business preset FAB */
|
|
14
|
+
export const CHAT_BUBBLE_SVG = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
|
15
|
+
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/>
|
|
16
|
+
</svg>`;
|
|
17
|
+
/** Close (X) icon */
|
|
18
|
+
export const CLOSE_SVG = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
|
19
|
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
|
20
|
+
</svg>`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ChatMessage, PochiDeskTheme, ToolUseEvent } from '../types';
|
|
3
|
+
import type { PresetConfig } from '../presets';
|
|
4
|
+
interface ChatPanelProps {
|
|
5
|
+
messages: ChatMessage[];
|
|
6
|
+
isStreaming: boolean;
|
|
7
|
+
onSend: (message: string) => void;
|
|
8
|
+
onClose?: () => void;
|
|
9
|
+
theme: PochiDeskTheme;
|
|
10
|
+
presetConfig: PresetConfig;
|
|
11
|
+
pendingToolCall: ToolUseEvent | null;
|
|
12
|
+
onConfirmToolCall: (toolCallId: string, approved: boolean) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const ChatPanel: React.FC<ChatPanelProps>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CLOSE_SVG } from '../assets/icons';
|
|
3
|
+
import { MessageList } from './MessageList';
|
|
4
|
+
import { MessageInput } from './MessageInput';
|
|
5
|
+
import { ToolConfirmDialog } from './ToolConfirmDialog';
|
|
6
|
+
export const ChatPanel = ({ messages, isStreaming, onSend, onClose, theme, presetConfig, pendingToolCall, onConfirmToolCall, }) => {
|
|
7
|
+
return (_jsxs("div", { role: "complementary", "aria-label": "\u30C1\u30E3\u30C3\u30C8\u30B5\u30DD\u30FC\u30C8", style: {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
width: '100%',
|
|
11
|
+
height: '100%',
|
|
12
|
+
backgroundColor: theme.backgroundColor,
|
|
13
|
+
borderRadius: `${theme.borderRadius}px`,
|
|
14
|
+
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.12)',
|
|
15
|
+
overflow: 'hidden',
|
|
16
|
+
position: 'relative',
|
|
17
|
+
fontFamily: theme.fontFamily,
|
|
18
|
+
}, children: [_jsxs("div", { style: {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'space-between',
|
|
22
|
+
padding: '14px 16px',
|
|
23
|
+
backgroundColor: theme.primaryColor,
|
|
24
|
+
color: '#ffffff',
|
|
25
|
+
fontWeight: 600,
|
|
26
|
+
fontSize: `${theme.fontSize + 2}px`,
|
|
27
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [presetConfig.showHeaderIcon && (_jsx("span", { style: { width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, dangerouslySetInnerHTML: { __html: presetConfig.headerIconSvg } })), _jsx("span", { children: presetConfig.headerTitle })] }), onClose && (_jsx("button", { type: "button", "aria-label": "\u30C1\u30E3\u30C3\u30C8\u3092\u9589\u3058\u308B", onClick: onClose, style: {
|
|
28
|
+
background: 'none',
|
|
29
|
+
border: 'none',
|
|
30
|
+
color: '#ffffff',
|
|
31
|
+
cursor: 'pointer',
|
|
32
|
+
padding: '4px',
|
|
33
|
+
minWidth: '44px',
|
|
34
|
+
minHeight: '44px',
|
|
35
|
+
display: 'flex',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
}, children: _jsx("span", { style: { width: '20px', height: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, dangerouslySetInnerHTML: { __html: CLOSE_SVG } }) }))] }), _jsx(MessageList, { messages: messages, theme: theme, presetConfig: presetConfig }), _jsx(MessageInput, { onSend: onSend, disabled: isStreaming, theme: theme }), pendingToolCall && (_jsx(ToolConfirmDialog, { toolCall: pendingToolCall, onConfirm: onConfirmToolCall, theme: theme })), _jsx("style", { children: `
|
|
39
|
+
@keyframes pochidesk-blink {
|
|
40
|
+
50% { opacity: 0; }
|
|
41
|
+
}
|
|
42
|
+
` })] }));
|
|
43
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { PochiDeskTheme } from '../types';
|
|
3
|
+
interface MessageInputProps {
|
|
4
|
+
onSend: (message: string) => void;
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
theme: PochiDeskTheme;
|
|
7
|
+
}
|
|
8
|
+
export declare const MessageInput: React.FC<MessageInputProps>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
|
+
export const MessageInput = ({ onSend, disabled, theme }) => {
|
|
4
|
+
const [value, setValue] = useState('');
|
|
5
|
+
const textareaRef = useRef(null);
|
|
6
|
+
const handleSubmit = () => {
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
if (!trimmed || disabled)
|
|
9
|
+
return;
|
|
10
|
+
onSend(trimmed);
|
|
11
|
+
setValue('');
|
|
12
|
+
if (textareaRef.current) {
|
|
13
|
+
textareaRef.current.style.height = 'auto';
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const handleKeyDown = (e) => {
|
|
17
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
handleSubmit();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const handleInput = (e) => {
|
|
23
|
+
setValue(e.target.value);
|
|
24
|
+
// Auto-resize
|
|
25
|
+
const textarea = e.target;
|
|
26
|
+
textarea.style.height = 'auto';
|
|
27
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
|
|
28
|
+
};
|
|
29
|
+
return (_jsxs("div", { style: {
|
|
30
|
+
display: 'flex',
|
|
31
|
+
alignItems: 'flex-end',
|
|
32
|
+
gap: '8px',
|
|
33
|
+
padding: '12px 16px',
|
|
34
|
+
borderTop: '1px solid #e5e7eb',
|
|
35
|
+
backgroundColor: theme.backgroundColor,
|
|
36
|
+
}, children: [_jsx("textarea", { ref: textareaRef, role: "textbox", "aria-label": "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u5165\u529B", value: value, onChange: handleInput, onKeyDown: handleKeyDown, disabled: disabled, placeholder: "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u5165\u529B...", rows: 1, style: {
|
|
37
|
+
flex: 1,
|
|
38
|
+
resize: 'none',
|
|
39
|
+
border: '1px solid #d1d5db',
|
|
40
|
+
borderRadius: `${theme.borderRadius}px`,
|
|
41
|
+
padding: '10px 12px',
|
|
42
|
+
fontFamily: theme.fontFamily,
|
|
43
|
+
fontSize: `${theme.fontSize}px`,
|
|
44
|
+
lineHeight: 1.5,
|
|
45
|
+
outline: 'none',
|
|
46
|
+
backgroundColor: '#ffffff',
|
|
47
|
+
color: theme.textColor,
|
|
48
|
+
maxHeight: '120px',
|
|
49
|
+
} }), _jsx("button", { type: "button", "aria-label": "\u9001\u4FE1", onClick: handleSubmit, disabled: disabled || !value.trim(), style: {
|
|
50
|
+
width: '44px',
|
|
51
|
+
height: '44px',
|
|
52
|
+
minWidth: '44px',
|
|
53
|
+
borderRadius: '50%',
|
|
54
|
+
border: 'none',
|
|
55
|
+
backgroundColor: disabled || !value.trim() ? '#d1d5db' : theme.primaryColor,
|
|
56
|
+
color: '#ffffff',
|
|
57
|
+
cursor: disabled || !value.trim() ? 'default' : 'pointer',
|
|
58
|
+
display: 'flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
justifyContent: 'center',
|
|
61
|
+
fontSize: '18px',
|
|
62
|
+
lineHeight: 1,
|
|
63
|
+
}, children: "\u25B6" })] }));
|
|
64
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ChatMessage, PochiDeskTheme } from '../types';
|
|
3
|
+
import type { PresetConfig } from '../presets';
|
|
4
|
+
interface MessageListProps {
|
|
5
|
+
messages: ChatMessage[];
|
|
6
|
+
theme: PochiDeskTheme;
|
|
7
|
+
presetConfig: PresetConfig;
|
|
8
|
+
}
|
|
9
|
+
export declare const MessageList: React.FC<MessageListProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
const avatarStyle = {
|
|
4
|
+
width: '28px',
|
|
5
|
+
height: '28px',
|
|
6
|
+
minWidth: '28px',
|
|
7
|
+
borderRadius: '50%',
|
|
8
|
+
display: 'flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
flexShrink: 0,
|
|
12
|
+
marginTop: '2px',
|
|
13
|
+
color: '#ffffff',
|
|
14
|
+
};
|
|
15
|
+
const avatarIconStyle = {
|
|
16
|
+
width: '18px',
|
|
17
|
+
height: '18px',
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'center',
|
|
21
|
+
};
|
|
22
|
+
export const MessageList = ({ messages, theme, presetConfig }) => {
|
|
23
|
+
const bottomRef = useRef(null);
|
|
24
|
+
const showAvatar = presetConfig.showAssistantAvatar;
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
27
|
+
}, [messages]);
|
|
28
|
+
const renderBubble = (msg) => (_jsxs("div", { style: {
|
|
29
|
+
maxWidth: showAvatar && msg.role === 'assistant' ? '100%' : '80%',
|
|
30
|
+
padding: '10px 14px',
|
|
31
|
+
borderRadius: `${theme.borderRadius}px`,
|
|
32
|
+
backgroundColor: msg.role === 'user' ? theme.userBubbleColor : theme.assistantBubbleColor,
|
|
33
|
+
color: msg.role === 'user' ? '#ffffff' : theme.textColor,
|
|
34
|
+
whiteSpace: 'pre-wrap',
|
|
35
|
+
wordBreak: 'break-word',
|
|
36
|
+
lineHeight: 1.5,
|
|
37
|
+
}, children: [msg.content, msg.isStreaming && (_jsx("span", { style: {
|
|
38
|
+
display: 'inline-block',
|
|
39
|
+
width: '6px',
|
|
40
|
+
height: '14px',
|
|
41
|
+
backgroundColor: 'currentColor',
|
|
42
|
+
marginLeft: '2px',
|
|
43
|
+
animation: 'pochidesk-blink 1s step-end infinite',
|
|
44
|
+
verticalAlign: 'text-bottom',
|
|
45
|
+
} }))] }));
|
|
46
|
+
return (_jsxs("div", { role: "log", "aria-live": "polite", style: {
|
|
47
|
+
flex: 1,
|
|
48
|
+
overflowY: 'auto',
|
|
49
|
+
padding: '16px',
|
|
50
|
+
display: 'flex',
|
|
51
|
+
flexDirection: 'column',
|
|
52
|
+
gap: '8px',
|
|
53
|
+
fontFamily: theme.fontFamily,
|
|
54
|
+
fontSize: `${theme.fontSize}px`,
|
|
55
|
+
color: theme.textColor,
|
|
56
|
+
}, children: [messages.length === 0 && (_jsx("div", { style: { textAlign: 'center', color: '#9ca3af', marginTop: '32px' }, children: "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u9001\u4FE1\u3057\u3066\u304F\u3060\u3055\u3044" })), messages.map((msg) => (_jsxs("div", { style: {
|
|
57
|
+
display: 'flex',
|
|
58
|
+
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
|
59
|
+
...(msg.role === 'assistant' && showAvatar ? { maxWidth: '85%' } : {}),
|
|
60
|
+
}, children: [msg.role === 'assistant' && showAvatar && (_jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '8px' }, children: [_jsx("span", { style: { ...avatarStyle, backgroundColor: theme.primaryColor }, children: _jsx("span", { style: avatarIconStyle, dangerouslySetInnerHTML: { __html: presetConfig.avatarIconSvg } }) }), renderBubble(msg)] })), msg.role === 'assistant' && !showAvatar && renderBubble(msg), msg.role === 'user' && renderBubble(msg)] }, msg.id))), _jsx("div", { ref: bottomRef })] }));
|
|
61
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ToolUseEvent, PochiDeskTheme } from '../types';
|
|
3
|
+
interface ToolConfirmDialogProps {
|
|
4
|
+
toolCall: ToolUseEvent;
|
|
5
|
+
onConfirm: (toolCallId: string, approved: boolean) => void;
|
|
6
|
+
theme: PochiDeskTheme;
|
|
7
|
+
}
|
|
8
|
+
export declare const ToolConfirmDialog: React.FC<ToolConfirmDialogProps>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const ToolConfirmDialog = ({ toolCall, onConfirm, theme, }) => {
|
|
3
|
+
return (_jsx("div", { role: "alertdialog", "aria-describedby": "pochidesk-tool-confirm-desc", style: {
|
|
4
|
+
position: 'absolute',
|
|
5
|
+
inset: 0,
|
|
6
|
+
display: 'flex',
|
|
7
|
+
alignItems: 'center',
|
|
8
|
+
justifyContent: 'center',
|
|
9
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
10
|
+
zIndex: 10,
|
|
11
|
+
}, children: _jsxs("div", { style: {
|
|
12
|
+
backgroundColor: theme.backgroundColor,
|
|
13
|
+
borderRadius: `${theme.borderRadius}px`,
|
|
14
|
+
padding: '24px',
|
|
15
|
+
maxWidth: '320px',
|
|
16
|
+
width: '90%',
|
|
17
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
18
|
+
fontFamily: theme.fontFamily,
|
|
19
|
+
fontSize: `${theme.fontSize}px`,
|
|
20
|
+
color: theme.textColor,
|
|
21
|
+
}, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: '12px' }, children: "\u30C4\u30FC\u30EB\u5B9F\u884C\u306E\u78BA\u8A8D" }), _jsxs("div", { id: "pochidesk-tool-confirm-desc", style: { marginBottom: '20px', lineHeight: 1.5 }, children: [_jsx("strong", { children: toolCall.name }), " \u3092\u5B9F\u884C\u3057\u307E\u3059\u304B\uFF1F"] }), _jsxs("div", { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' }, children: [_jsx("button", { type: "button", onClick: () => onConfirm(toolCall.tool_use_id, false), style: {
|
|
22
|
+
padding: '8px 16px',
|
|
23
|
+
border: '1px solid #d1d5db',
|
|
24
|
+
borderRadius: `${theme.borderRadius}px`,
|
|
25
|
+
backgroundColor: '#ffffff',
|
|
26
|
+
color: theme.textColor,
|
|
27
|
+
cursor: 'pointer',
|
|
28
|
+
fontFamily: theme.fontFamily,
|
|
29
|
+
fontSize: `${theme.fontSize}px`,
|
|
30
|
+
minWidth: '44px',
|
|
31
|
+
minHeight: '44px',
|
|
32
|
+
}, children: "\u62D2\u5426" }), _jsx("button", { type: "button", onClick: () => onConfirm(toolCall.tool_use_id, true), style: {
|
|
33
|
+
padding: '8px 16px',
|
|
34
|
+
border: 'none',
|
|
35
|
+
borderRadius: `${theme.borderRadius}px`,
|
|
36
|
+
backgroundColor: theme.primaryColor,
|
|
37
|
+
color: '#ffffff',
|
|
38
|
+
cursor: 'pointer',
|
|
39
|
+
fontFamily: theme.fontFamily,
|
|
40
|
+
fontSize: `${theme.fontSize}px`,
|
|
41
|
+
minWidth: '44px',
|
|
42
|
+
minHeight: '44px',
|
|
43
|
+
}, children: "\u627F\u8A8D" })] })] }) }));
|
|
44
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ChatMessage, PochiDeskError } from '../types';
|
|
2
|
+
interface UseChatMessagesReturn {
|
|
3
|
+
messages: ChatMessage[];
|
|
4
|
+
isLoading: boolean;
|
|
5
|
+
error: PochiDeskError | null;
|
|
6
|
+
refetch: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare function useChatMessages(botId: string, token: string, apiUrl: string | undefined, sessionId: string | null): UseChatMessagesReturn;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
export function useChatMessages(botId, token, apiUrl = '', sessionId) {
|
|
3
|
+
const [messages, setMessages] = useState([]);
|
|
4
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
const baseUrl = `${apiUrl}/api/v1/chat/support/${botId}`;
|
|
7
|
+
const loadMessages = useCallback(async () => {
|
|
8
|
+
if (!sessionId)
|
|
9
|
+
return;
|
|
10
|
+
setIsLoading(true);
|
|
11
|
+
setError(null);
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(`${baseUrl}/sessions/${sessionId}/messages`, {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${token}`,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const body = await res.json().catch(() => ({}));
|
|
21
|
+
setError({
|
|
22
|
+
code: body.error?.code ?? 'REQUEST_FAILED',
|
|
23
|
+
message: body.error?.message ?? `HTTP ${res.status}`,
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const body = (await res.json());
|
|
28
|
+
setMessages(body.data);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
setError({
|
|
32
|
+
code: 'NETWORK_ERROR',
|
|
33
|
+
message: err instanceof Error ? err.message : String(err),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setIsLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}, [baseUrl, sessionId, token]);
|
|
40
|
+
// Auto-load when sessionId changes
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (sessionId) {
|
|
43
|
+
loadMessages();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
setMessages([]);
|
|
47
|
+
}
|
|
48
|
+
}, [sessionId, loadMessages]);
|
|
49
|
+
return { messages, isLoading, error, refetch: loadMessages };
|
|
50
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
const STORAGE_KEY_PREFIX = 'pochidesk_session_';
|
|
3
|
+
function getStoredSession(botId) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = localStorage.getItem(`${STORAGE_KEY_PREFIX}${botId}`);
|
|
6
|
+
if (!raw)
|
|
7
|
+
return null;
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function storeSession(botId, stored) {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem(`${STORAGE_KEY_PREFIX}${botId}`, JSON.stringify(stored));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// localStorage unavailable
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function removeStoredSession(botId) {
|
|
23
|
+
try {
|
|
24
|
+
localStorage.removeItem(`${STORAGE_KEY_PREFIX}${botId}`);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// localStorage unavailable
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function useChatSession(botId, token, apiUrl = '') {
|
|
31
|
+
const [session, setSession] = useState(null);
|
|
32
|
+
const [sessions, setSessions] = useState([]);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
34
|
+
const [error, setError] = useState(null);
|
|
35
|
+
const baseUrl = `${apiUrl}/api/v1/chat/support/${botId}`;
|
|
36
|
+
const authHeaders = {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
Authorization: `Bearer ${token}`,
|
|
39
|
+
};
|
|
40
|
+
// Restore session from localStorage on mount
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const stored = getStoredSession(botId);
|
|
43
|
+
if (stored) {
|
|
44
|
+
setSession({
|
|
45
|
+
id: stored.sessionId,
|
|
46
|
+
botId,
|
|
47
|
+
createdAt: stored.createdAt,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, [botId]);
|
|
51
|
+
const createSession = useCallback(async () => {
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${baseUrl}/sessions`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: authHeaders,
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const body = await res.json().catch(() => ({}));
|
|
61
|
+
const err = {
|
|
62
|
+
code: body.error?.code ?? 'REQUEST_FAILED',
|
|
63
|
+
message: body.error?.message ?? `HTTP ${res.status}`,
|
|
64
|
+
};
|
|
65
|
+
setError(err);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const body = (await res.json());
|
|
69
|
+
const newSession = body.data;
|
|
70
|
+
setSession(newSession);
|
|
71
|
+
storeSession(botId, {
|
|
72
|
+
sessionId: newSession.id,
|
|
73
|
+
createdAt: newSession.createdAt,
|
|
74
|
+
lastMessageAt: newSession.createdAt,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
setError({
|
|
79
|
+
code: 'NETWORK_ERROR',
|
|
80
|
+
message: err instanceof Error ? err.message : String(err),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
setIsLoading(false);
|
|
85
|
+
}
|
|
86
|
+
}, [baseUrl, botId, token]);
|
|
87
|
+
const loadSessions = useCallback(async () => {
|
|
88
|
+
setIsLoading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${baseUrl}/sessions`, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
headers: authHeaders,
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const body = await res.json().catch(() => ({}));
|
|
97
|
+
const err = {
|
|
98
|
+
code: body.error?.code ?? 'REQUEST_FAILED',
|
|
99
|
+
message: body.error?.message ?? `HTTP ${res.status}`,
|
|
100
|
+
};
|
|
101
|
+
setError(err);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const body = (await res.json());
|
|
105
|
+
setSessions(body.data);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
setError({
|
|
109
|
+
code: 'NETWORK_ERROR',
|
|
110
|
+
message: err instanceof Error ? err.message : String(err),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
setIsLoading(false);
|
|
115
|
+
}
|
|
116
|
+
}, [baseUrl, token]);
|
|
117
|
+
const selectSession = useCallback((sessionId) => {
|
|
118
|
+
const found = sessions.find((s) => s.id === sessionId);
|
|
119
|
+
if (found) {
|
|
120
|
+
setSession(found);
|
|
121
|
+
storeSession(botId, {
|
|
122
|
+
sessionId: found.id,
|
|
123
|
+
createdAt: found.createdAt,
|
|
124
|
+
lastMessageAt: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Allow selecting a session not yet in the list
|
|
129
|
+
setSession({ id: sessionId, botId, createdAt: new Date().toISOString() });
|
|
130
|
+
storeSession(botId, {
|
|
131
|
+
sessionId,
|
|
132
|
+
createdAt: new Date().toISOString(),
|
|
133
|
+
lastMessageAt: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}, [sessions, botId]);
|
|
137
|
+
return { session, sessions, isLoading, error, createSession, loadSessions, selectSession };
|
|
138
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { connectSSE } from '../lib/sse-client';
|
|
3
|
+
export function useStreaming(botId, token, apiUrl = '', sessionId, onToolCall) {
|
|
4
|
+
const [messages, setMessages] = useState([]);
|
|
5
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
6
|
+
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
7
|
+
const abortRef = useRef(null);
|
|
8
|
+
const streamingContentRef = useRef('');
|
|
9
|
+
const baseUrl = `${apiUrl}/api/v1/chat/support/${botId}`;
|
|
10
|
+
const sendMessage = useCallback(async (message) => {
|
|
11
|
+
if (!sessionId || isStreaming)
|
|
12
|
+
return;
|
|
13
|
+
// Add user message
|
|
14
|
+
const userMessage = {
|
|
15
|
+
id: `user-${Date.now()}`,
|
|
16
|
+
role: 'user',
|
|
17
|
+
content: message,
|
|
18
|
+
createdAt: new Date().toISOString(),
|
|
19
|
+
};
|
|
20
|
+
// Add placeholder assistant message for streaming
|
|
21
|
+
const assistantId = `assistant-${Date.now()}`;
|
|
22
|
+
const assistantMessage = {
|
|
23
|
+
id: assistantId,
|
|
24
|
+
role: 'assistant',
|
|
25
|
+
content: '',
|
|
26
|
+
createdAt: new Date().toISOString(),
|
|
27
|
+
isStreaming: true,
|
|
28
|
+
};
|
|
29
|
+
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
|
30
|
+
setIsStreaming(true);
|
|
31
|
+
streamingContentRef.current = '';
|
|
32
|
+
// Abort previous connection if any
|
|
33
|
+
abortRef.current?.abort();
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
abortRef.current = controller;
|
|
36
|
+
const url = `${baseUrl}/sessions/${sessionId}/messages`;
|
|
37
|
+
const headers = {
|
|
38
|
+
Authorization: `Bearer ${token}`,
|
|
39
|
+
};
|
|
40
|
+
await connectSSE(url, { content: message }, headers, (event) => {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(event.data);
|
|
43
|
+
switch (event.event) {
|
|
44
|
+
case 'content_delta': {
|
|
45
|
+
const delta = parsed;
|
|
46
|
+
streamingContentRef.current += delta.delta;
|
|
47
|
+
const content = streamingContentRef.current;
|
|
48
|
+
setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content } : msg));
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'tool_use': {
|
|
52
|
+
const toolEvent = parsed;
|
|
53
|
+
if (toolEvent.execution_mode === 'client' && onToolCall) {
|
|
54
|
+
onToolCall(toolEvent.name, toolEvent.input);
|
|
55
|
+
}
|
|
56
|
+
if (toolEvent.requires_confirmation) {
|
|
57
|
+
setPendingToolCall(toolEvent);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'message_end': {
|
|
62
|
+
const endEvent = parsed;
|
|
63
|
+
setMessages((prev) => prev.map((msg) => msg.id === assistantId
|
|
64
|
+
? { ...msg, id: endEvent.message_id, isStreaming: false }
|
|
65
|
+
: msg));
|
|
66
|
+
setIsStreaming(false);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case 'error': {
|
|
70
|
+
const errorEvent = parsed;
|
|
71
|
+
setMessages((prev) => prev.map((msg) => msg.id === assistantId
|
|
72
|
+
? { ...msg, content: `Error: ${errorEvent.message}`, isStreaming: false }
|
|
73
|
+
: msg));
|
|
74
|
+
setIsStreaming(false);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
// tool_result and ping are handled silently
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Ignore malformed events
|
|
82
|
+
}
|
|
83
|
+
}, (_error) => {
|
|
84
|
+
setIsStreaming(false);
|
|
85
|
+
setMessages((prev) => prev.map((msg) => msg.id === assistantId
|
|
86
|
+
? { ...msg, isStreaming: false }
|
|
87
|
+
: msg));
|
|
88
|
+
}, controller.signal);
|
|
89
|
+
}, [sessionId, isStreaming, baseUrl, token, onToolCall]);
|
|
90
|
+
const confirmToolCall = useCallback(async (toolCallId, approved) => {
|
|
91
|
+
if (!sessionId)
|
|
92
|
+
return;
|
|
93
|
+
setPendingToolCall(null);
|
|
94
|
+
const url = `${baseUrl}/sessions/${sessionId}/tool-confirm`;
|
|
95
|
+
const headers = {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
Authorization: `Bearer ${token}`,
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
await fetch(url, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers,
|
|
103
|
+
body: JSON.stringify({ tool_use_id: toolCallId, approved }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Error handled silently; the SSE stream will report issues
|
|
108
|
+
}
|
|
109
|
+
}, [sessionId, baseUrl, token]);
|
|
110
|
+
return { messages, isStreaming, sendMessage, pendingToolCall, confirmToolCall };
|
|
111
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PochiDesk } from './PochiDesk';
|
|
2
|
+
export { useChatSession } from './hooks/useChatSession';
|
|
3
|
+
export { useStreaming } from './hooks/useStreaming';
|
|
4
|
+
export { useChatMessages } from './hooks/useChatMessages';
|
|
5
|
+
export type { PresetName } from './presets';
|
|
6
|
+
export type { PochiDeskProps, PochiDeskTheme, PochiDeskError, ChatMessage, ChatSession, SSEEvent, ContentDeltaEvent, ToolUseEvent, ToolResultEvent, MessageEndEvent, ErrorEvent, StoredSession, UseChatSessionReturn, UseStreamingReturn, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export async function connectSSE(url, body, headers, onEvent, onError, signal) {
|
|
2
|
+
let response;
|
|
3
|
+
try {
|
|
4
|
+
response = await fetch(url, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
...headers,
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify(body),
|
|
11
|
+
signal,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!response.ok || !response.body) {
|
|
19
|
+
onError(new Error(`SSE connection failed: ${response.status}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const reader = response.body.getReader();
|
|
23
|
+
const decoder = new TextDecoder();
|
|
24
|
+
let buffer = '';
|
|
25
|
+
try {
|
|
26
|
+
while (true) {
|
|
27
|
+
const { done, value } = await reader.read();
|
|
28
|
+
if (done)
|
|
29
|
+
break;
|
|
30
|
+
buffer += decoder.decode(value, { stream: true });
|
|
31
|
+
const lines = buffer.split('\n');
|
|
32
|
+
buffer = lines.pop() ?? '';
|
|
33
|
+
let currentEvent = '';
|
|
34
|
+
let currentData = '';
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (line.startsWith('event: ')) {
|
|
37
|
+
currentEvent = line.slice(7);
|
|
38
|
+
}
|
|
39
|
+
else if (line.startsWith('data: ')) {
|
|
40
|
+
currentData = line.slice(6);
|
|
41
|
+
}
|
|
42
|
+
else if (line === '' && currentEvent) {
|
|
43
|
+
onEvent({ event: currentEvent, data: currentData });
|
|
44
|
+
currentEvent = '';
|
|
45
|
+
currentData = '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (signal?.aborted)
|
|
52
|
+
return;
|
|
53
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PochiDeskTheme } from '../types';
|
|
2
|
+
export type PresetName = 'pochi' | 'business';
|
|
3
|
+
export interface PresetConfig {
|
|
4
|
+
/** Base theme values */
|
|
5
|
+
theme: PochiDeskTheme;
|
|
6
|
+
/** Header title text */
|
|
7
|
+
headerTitle: string;
|
|
8
|
+
/** Show avatar icon next to assistant messages */
|
|
9
|
+
showAssistantAvatar: boolean;
|
|
10
|
+
/** Show icon in header */
|
|
11
|
+
showHeaderIcon: boolean;
|
|
12
|
+
/** SVG string for FAB button (open state) */
|
|
13
|
+
fabIconSvg: string;
|
|
14
|
+
/** SVG string for assistant avatar */
|
|
15
|
+
avatarIconSvg: string;
|
|
16
|
+
/** SVG string for header icon */
|
|
17
|
+
headerIconSvg: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolvePreset(presetName?: PresetName, themeOverride?: Partial<PochiDeskTheme>): {
|
|
20
|
+
theme: PochiDeskTheme;
|
|
21
|
+
preset: PresetConfig;
|
|
22
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { POCHI_DOG_SVG, CHAT_BUBBLE_SVG } from '../assets/icons';
|
|
2
|
+
const POCHI_PRESET = {
|
|
3
|
+
theme: {
|
|
4
|
+
primaryColor: '#6366f1',
|
|
5
|
+
textColor: '#1f2937',
|
|
6
|
+
backgroundColor: '#ffffff',
|
|
7
|
+
userBubbleColor: '#6366f1',
|
|
8
|
+
assistantBubbleColor: '#f3f4f6',
|
|
9
|
+
fontFamily: '"Noto Sans JP", "Helvetica Neue", Arial, sans-serif',
|
|
10
|
+
fontSize: 14,
|
|
11
|
+
borderRadius: 12,
|
|
12
|
+
panelWidth: 400,
|
|
13
|
+
panelHeight: 600,
|
|
14
|
+
},
|
|
15
|
+
headerTitle: 'ポチに聞いてみよう!',
|
|
16
|
+
showAssistantAvatar: true,
|
|
17
|
+
showHeaderIcon: true,
|
|
18
|
+
fabIconSvg: POCHI_DOG_SVG,
|
|
19
|
+
avatarIconSvg: POCHI_DOG_SVG,
|
|
20
|
+
headerIconSvg: POCHI_DOG_SVG,
|
|
21
|
+
};
|
|
22
|
+
const BUSINESS_PRESET = {
|
|
23
|
+
theme: {
|
|
24
|
+
primaryColor: '#1e293b',
|
|
25
|
+
textColor: '#334155',
|
|
26
|
+
backgroundColor: '#ffffff',
|
|
27
|
+
userBubbleColor: '#1e293b',
|
|
28
|
+
assistantBubbleColor: '#f1f5f9',
|
|
29
|
+
fontFamily: '"Noto Sans JP", "Helvetica Neue", Arial, sans-serif',
|
|
30
|
+
fontSize: 14,
|
|
31
|
+
borderRadius: 8,
|
|
32
|
+
panelWidth: 400,
|
|
33
|
+
panelHeight: 600,
|
|
34
|
+
},
|
|
35
|
+
headerTitle: 'チャットサポート',
|
|
36
|
+
showAssistantAvatar: false,
|
|
37
|
+
showHeaderIcon: false,
|
|
38
|
+
fabIconSvg: CHAT_BUBBLE_SVG,
|
|
39
|
+
avatarIconSvg: '',
|
|
40
|
+
headerIconSvg: '',
|
|
41
|
+
};
|
|
42
|
+
const PRESETS = {
|
|
43
|
+
pochi: POCHI_PRESET,
|
|
44
|
+
business: BUSINESS_PRESET,
|
|
45
|
+
};
|
|
46
|
+
export function resolvePreset(presetName, themeOverride) {
|
|
47
|
+
const preset = PRESETS[presetName ?? 'pochi'];
|
|
48
|
+
const theme = { ...preset.theme, ...themeOverride };
|
|
49
|
+
return { theme, preset };
|
|
50
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { PresetName } from './presets';
|
|
2
|
+
export interface PochiDeskProps {
|
|
3
|
+
botId: string;
|
|
4
|
+
token: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
position?: 'bottom-right' | 'bottom-left' | 'inline';
|
|
7
|
+
preset?: PresetName;
|
|
8
|
+
theme?: Partial<PochiDeskTheme>;
|
|
9
|
+
onToolCall?: (toolName: string, params: Record<string, unknown>) => void;
|
|
10
|
+
onSessionCreate?: (sessionId: string) => void;
|
|
11
|
+
onError?: (error: PochiDeskError) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface PochiDeskTheme {
|
|
14
|
+
primaryColor: string;
|
|
15
|
+
textColor: string;
|
|
16
|
+
backgroundColor: string;
|
|
17
|
+
userBubbleColor: string;
|
|
18
|
+
assistantBubbleColor: string;
|
|
19
|
+
fontFamily: string;
|
|
20
|
+
fontSize: number;
|
|
21
|
+
borderRadius: number;
|
|
22
|
+
panelWidth: number;
|
|
23
|
+
panelHeight: number;
|
|
24
|
+
}
|
|
25
|
+
export interface PochiDeskError {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ChatMessage {
|
|
30
|
+
id: string;
|
|
31
|
+
role: 'user' | 'assistant';
|
|
32
|
+
content: string;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
isStreaming?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ChatSession {
|
|
37
|
+
id: string;
|
|
38
|
+
botId: string;
|
|
39
|
+
userId?: string;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SSEEvent {
|
|
43
|
+
event: string;
|
|
44
|
+
data: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ContentDeltaEvent {
|
|
47
|
+
type: 'content_delta';
|
|
48
|
+
delta: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ToolUseEvent {
|
|
51
|
+
type: 'tool_use';
|
|
52
|
+
tool_use_id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
input: Record<string, unknown>;
|
|
55
|
+
requires_confirmation: boolean;
|
|
56
|
+
execution_mode: 'server' | 'client';
|
|
57
|
+
}
|
|
58
|
+
export interface ToolResultEvent {
|
|
59
|
+
type: 'tool_result';
|
|
60
|
+
tool_use_id: string;
|
|
61
|
+
result: unknown;
|
|
62
|
+
is_error: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface MessageEndEvent {
|
|
65
|
+
type: 'message_end';
|
|
66
|
+
message_id: string;
|
|
67
|
+
usage: {
|
|
68
|
+
input_tokens: number;
|
|
69
|
+
output_tokens: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface ErrorEvent {
|
|
73
|
+
type: 'error';
|
|
74
|
+
code: string;
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
export interface StoredSession {
|
|
78
|
+
sessionId: string;
|
|
79
|
+
createdAt: string;
|
|
80
|
+
lastMessageAt: string;
|
|
81
|
+
}
|
|
82
|
+
export interface UseChatSessionReturn {
|
|
83
|
+
session: ChatSession | null;
|
|
84
|
+
sessions: ChatSession[];
|
|
85
|
+
isLoading: boolean;
|
|
86
|
+
error: PochiDeskError | null;
|
|
87
|
+
createSession: () => Promise<void>;
|
|
88
|
+
loadSessions: () => Promise<void>;
|
|
89
|
+
selectSession: (sessionId: string) => void;
|
|
90
|
+
}
|
|
91
|
+
export interface UseStreamingReturn {
|
|
92
|
+
messages: ChatMessage[];
|
|
93
|
+
isStreaming: boolean;
|
|
94
|
+
sendMessage: (message: string) => Promise<void>;
|
|
95
|
+
pendingToolCall: ToolUseEvent | null;
|
|
96
|
+
confirmToolCall: (toolCallId: string, approved: boolean) => Promise<void>;
|
|
97
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haro/pochidesk-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PochiDesk React SDK — AIチャットボットをReact/Next.jsに簡単統合",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18.0.0",
|
|
24
|
+
"react-dom": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^19.0.0",
|
|
28
|
+
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"react": "^19.0.0",
|
|
30
|
+
"react-dom": "^19.0.0",
|
|
31
|
+
"typescript": "^5.8.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"chatbot",
|
|
35
|
+
"ai",
|
|
36
|
+
"claude",
|
|
37
|
+
"react",
|
|
38
|
+
"nextjs",
|
|
39
|
+
"support",
|
|
40
|
+
"pochidesk",
|
|
41
|
+
"haro"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/Haronoya/pochidesk.git",
|
|
47
|
+
"directory": "packages/react"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://pochidesk.com"
|
|
50
|
+
}
|