@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.
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { ChatBridgeProps } from './types';
3
+ export declare const ChatBridge: React.FC<ChatBridgeProps>;
@@ -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,3 @@
1
+ import React from 'react';
2
+ import type { PochiDeskProps } from './types';
3
+ export declare const PochiDesk: React.FC<PochiDeskProps>;
@@ -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,2 @@
1
+ import type { UseChatSessionReturn } from '../types';
2
+ export declare function useChatSession(botId: string, token: string, apiUrl?: string): UseChatSessionReturn;
@@ -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,2 @@
1
+ import type { UseStreamingReturn } from '../types';
2
+ export declare function useStreaming(botId: string, token: string, apiUrl: string | undefined, sessionId: string | null, onToolCall?: (toolName: string, params: Record<string, unknown>) => void): UseStreamingReturn;
@@ -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
+ }
@@ -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,4 @@
1
+ export { PochiDesk } from './PochiDesk';
2
+ export { useChatSession } from './hooks/useChatSession';
3
+ export { useStreaming } from './hooks/useStreaming';
4
+ export { useChatMessages } from './hooks/useChatMessages';
@@ -0,0 +1,2 @@
1
+ import type { SSEEvent } from '../types';
2
+ export declare function connectSSE(url: string, body: Record<string, unknown>, headers: Record<string, string>, onEvent: (event: SSEEvent) => void, onError: (error: Error) => void, signal?: AbortSignal): Promise<void>;
@@ -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
+ }
@@ -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
+ }