@meechi-ai/core 1.0.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/LICENSE +624 -0
- package/README.md +59 -0
- package/dist/components/CalendarView.d.ts +3 -0
- package/dist/components/CalendarView.js +72 -0
- package/dist/components/ChatInterface.d.ts +6 -0
- package/dist/components/ChatInterface.js +105 -0
- package/dist/components/FileExplorer.d.ts +9 -0
- package/dist/components/FileExplorer.js +757 -0
- package/dist/components/Icon.d.ts +9 -0
- package/dist/components/Icon.js +44 -0
- package/dist/components/SourceEditor.d.ts +13 -0
- package/dist/components/SourceEditor.js +50 -0
- package/dist/components/ThemeProvider.d.ts +5 -0
- package/dist/components/ThemeProvider.js +105 -0
- package/dist/components/ThemeSwitcher.d.ts +1 -0
- package/dist/components/ThemeSwitcher.js +16 -0
- package/dist/components/voice/VoiceInputArea.d.ts +14 -0
- package/dist/components/voice/VoiceInputArea.js +190 -0
- package/dist/components/voice/VoiceOverlay.d.ts +7 -0
- package/dist/components/voice/VoiceOverlay.js +71 -0
- package/dist/hooks/useMeechi.d.ts +16 -0
- package/dist/hooks/useMeechi.js +461 -0
- package/dist/hooks/useSync.d.ts +8 -0
- package/dist/hooks/useSync.js +87 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/lib/ai/embeddings.d.ts +15 -0
- package/dist/lib/ai/embeddings.js +128 -0
- package/dist/lib/ai/gpu-lock.d.ts +19 -0
- package/dist/lib/ai/gpu-lock.js +43 -0
- package/dist/lib/ai/llm.worker.d.ts +1 -0
- package/dist/lib/ai/llm.worker.js +7 -0
- package/dist/lib/ai/local-llm.d.ts +30 -0
- package/dist/lib/ai/local-llm.js +211 -0
- package/dist/lib/ai/manager.d.ts +20 -0
- package/dist/lib/ai/manager.js +51 -0
- package/dist/lib/ai/parsing.d.ts +12 -0
- package/dist/lib/ai/parsing.js +56 -0
- package/dist/lib/ai/prompts.d.ts +2 -0
- package/dist/lib/ai/prompts.js +2 -0
- package/dist/lib/ai/providers/gemini.d.ts +6 -0
- package/dist/lib/ai/providers/gemini.js +88 -0
- package/dist/lib/ai/providers/groq.d.ts +6 -0
- package/dist/lib/ai/providers/groq.js +42 -0
- package/dist/lib/ai/registry.d.ts +29 -0
- package/dist/lib/ai/registry.js +52 -0
- package/dist/lib/ai/tools.d.ts +2 -0
- package/dist/lib/ai/tools.js +106 -0
- package/dist/lib/ai/types.d.ts +22 -0
- package/dist/lib/ai/types.js +1 -0
- package/dist/lib/ai/worker.d.ts +1 -0
- package/dist/lib/ai/worker.js +60 -0
- package/dist/lib/audio/input.d.ts +13 -0
- package/dist/lib/audio/input.js +121 -0
- package/dist/lib/audio/stt.d.ts +13 -0
- package/dist/lib/audio/stt.js +119 -0
- package/dist/lib/audio/tts.d.ts +12 -0
- package/dist/lib/audio/tts.js +128 -0
- package/dist/lib/audio/vad.d.ts +18 -0
- package/dist/lib/audio/vad.js +117 -0
- package/dist/lib/colors.d.ts +16 -0
- package/dist/lib/colors.js +67 -0
- package/dist/lib/extensions.d.ts +35 -0
- package/dist/lib/extensions.js +24 -0
- package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
- package/dist/lib/hooks/use-voice-loop.js +313 -0
- package/dist/lib/mcp/McpClient.d.ts +19 -0
- package/dist/lib/mcp/McpClient.js +42 -0
- package/dist/lib/mcp/McpRegistry.d.ts +47 -0
- package/dist/lib/mcp/McpRegistry.js +117 -0
- package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
- package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
- package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
- package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
- package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
- package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
- package/dist/lib/mcp/native/index.d.ts +10 -0
- package/dist/lib/mcp/native/index.js +10 -0
- package/dist/lib/mcp/types.d.ts +35 -0
- package/dist/lib/mcp/types.js +1 -0
- package/dist/lib/pdf.d.ts +10 -0
- package/dist/lib/pdf.js +142 -0
- package/dist/lib/settings.d.ts +48 -0
- package/dist/lib/settings.js +87 -0
- package/dist/lib/storage/db.d.ts +57 -0
- package/dist/lib/storage/db.js +45 -0
- package/dist/lib/storage/local.d.ts +28 -0
- package/dist/lib/storage/local.js +534 -0
- package/dist/lib/storage/migrate.d.ts +3 -0
- package/dist/lib/storage/migrate.js +122 -0
- package/dist/lib/storage/types.d.ts +66 -0
- package/dist/lib/storage/types.js +1 -0
- package/dist/lib/sync/client-drive.d.ts +9 -0
- package/dist/lib/sync/client-drive.js +69 -0
- package/dist/lib/sync/engine.d.ts +18 -0
- package/dist/lib/sync/engine.js +517 -0
- package/dist/lib/sync/google-drive.d.ts +52 -0
- package/dist/lib/sync/google-drive.js +183 -0
- package/dist/lib/sync/merge.d.ts +1 -0
- package/dist/lib/sync/merge.js +68 -0
- package/dist/lib/yjs/YjsProvider.d.ts +11 -0
- package/dist/lib/yjs/YjsProvider.js +33 -0
- package/dist/lib/yjs/graph.d.ts +11 -0
- package/dist/lib/yjs/graph.js +7 -0
- package/dist/lib/yjs/hooks.d.ts +7 -0
- package/dist/lib/yjs/hooks.js +37 -0
- package/dist/lib/yjs/store.d.ts +4 -0
- package/dist/lib/yjs/store.js +19 -0
- package/dist/lib/yjs/syncGraph.d.ts +1 -0
- package/dist/lib/yjs/syncGraph.js +38 -0
- package/dist/providers/theme-provider.d.ts +3 -0
- package/dist/providers/theme-provider.js +18 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface IconProps extends React.SVGProps<SVGSVGElement> {
|
|
3
|
+
name: string;
|
|
4
|
+
size?: number | string;
|
|
5
|
+
className?: string;
|
|
6
|
+
color?: string;
|
|
7
|
+
}
|
|
8
|
+
export default function Icon({ name, size, className, color, ...props }: IconProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import * as LucideIcons from 'lucide-react';
|
|
16
|
+
import { settingsManager } from '@/lib/settings';
|
|
17
|
+
export default function Icon(_a) {
|
|
18
|
+
var { name, size = 16, className, color } = _a, props = __rest(_a, ["name", "size", "className", "color"]);
|
|
19
|
+
const [library, setLibrary] = React.useState('lucide');
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
async function load() {
|
|
22
|
+
var _a;
|
|
23
|
+
const cfg = await settingsManager.getConfig();
|
|
24
|
+
setLibrary(((_a = cfg.appearance) === null || _a === void 0 ? void 0 : _a.iconLibrary) || 'lucide');
|
|
25
|
+
}
|
|
26
|
+
// load();
|
|
27
|
+
}, []);
|
|
28
|
+
// Normalize Name (e.g. "search" -> "Search", "file-text" -> "FileText")
|
|
29
|
+
// Lucide exports PascalCase.
|
|
30
|
+
const pascalName = name.split(/[-_]/).map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
|
31
|
+
if (library === 'lucide') {
|
|
32
|
+
const LucideIcon = LucideIcons[pascalName] || LucideIcons[name];
|
|
33
|
+
if (!LucideIcon) {
|
|
34
|
+
console.warn(`Icon not found: ${name} (${pascalName})`);
|
|
35
|
+
return _jsx("span", { style: { width: size, height: size, display: 'inline-block', background: '#ccc' } });
|
|
36
|
+
}
|
|
37
|
+
// LucideIcon accepts LucideProps which are compatible with SVGProps
|
|
38
|
+
return _jsx(LucideIcon, Object.assign({ size: size, className: className, color: color }, props));
|
|
39
|
+
}
|
|
40
|
+
if (library === 'material') {
|
|
41
|
+
return _jsxs("span", { children: ["M-", name] }); // Placeholder
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface SourceEditorProps {
|
|
3
|
+
file: {
|
|
4
|
+
content: string;
|
|
5
|
+
path: string;
|
|
6
|
+
metadata?: any;
|
|
7
|
+
};
|
|
8
|
+
isEditing: boolean;
|
|
9
|
+
onSave?: (content: string) => void;
|
|
10
|
+
onRenderExtension?: (editor: any) => React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export default function SourceEditor({ file, isEditing, onSave, onRenderExtension }: SourceEditorProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
4
|
+
import { Markdown } from 'tiptap-markdown';
|
|
5
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
6
|
+
import { TextStyle } from '@tiptap/extension-text-style';
|
|
7
|
+
import { Color } from '@tiptap/extension-color';
|
|
8
|
+
import Link from '@tiptap/extension-link';
|
|
9
|
+
import { useEffect } from 'react';
|
|
10
|
+
export default function SourceEditor({ file, isEditing, onSave, onRenderExtension }) {
|
|
11
|
+
const editor = useEditor({
|
|
12
|
+
extensions: [
|
|
13
|
+
StarterKit,
|
|
14
|
+
Markdown.configure({
|
|
15
|
+
html: true,
|
|
16
|
+
transformPastedText: true,
|
|
17
|
+
transformCopiedText: true,
|
|
18
|
+
}),
|
|
19
|
+
Placeholder.configure({
|
|
20
|
+
placeholder: 'Start typing...'
|
|
21
|
+
}),
|
|
22
|
+
TextStyle,
|
|
23
|
+
Color,
|
|
24
|
+
Link.configure({
|
|
25
|
+
openOnClick: false
|
|
26
|
+
})
|
|
27
|
+
],
|
|
28
|
+
content: file.content,
|
|
29
|
+
editable: isEditing,
|
|
30
|
+
onUpdate: ({ editor }) => {
|
|
31
|
+
// Optional: Auto-save or debounce logic
|
|
32
|
+
},
|
|
33
|
+
editorProps: {
|
|
34
|
+
attributes: {
|
|
35
|
+
class: 'prose dark:prose-invert max-w-none focus:outline-none min-h-[200px]'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Sync content if file changes externally
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (editor && file.content !== editor.storage.markdown.getMarkdown()) {
|
|
42
|
+
editor.commands.setContent(file.content);
|
|
43
|
+
}
|
|
44
|
+
}, [file.path, editor]);
|
|
45
|
+
// Save on Unmount or explicit save
|
|
46
|
+
// (Simplified for now)
|
|
47
|
+
if (!editor)
|
|
48
|
+
return null;
|
|
49
|
+
return (_jsxs("div", { className: "source-editor", children: [_jsx("div", { className: "toolbar", style: { padding: '0.5rem', borderBottom: '1px solid #eee' }, children: onRenderExtension && onRenderExtension(editor) }), _jsx(EditorContent, { editor: editor, style: { padding: '1rem' } })] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
3
|
+
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
|
|
4
|
+
export declare function ThemeProvider({ children, ...props }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
16
|
+
import { settingsManager } from "@/lib/settings";
|
|
17
|
+
import { getOklch } from "@/lib/colors";
|
|
18
|
+
export function ThemeProvider(_a) {
|
|
19
|
+
var { children } = _a, props = __rest(_a, ["children"]);
|
|
20
|
+
const [mounted, setMounted] = React.useState(false);
|
|
21
|
+
// Appearance Sync
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
setMounted(true);
|
|
24
|
+
async function loadAppearance() {
|
|
25
|
+
// Load initial appearance settings
|
|
26
|
+
const config = await settingsManager.getConfig();
|
|
27
|
+
applyAppearance(config.appearance);
|
|
28
|
+
}
|
|
29
|
+
loadAppearance();
|
|
30
|
+
// Listen to storage changes for multi-tab sync (and settings page updates)
|
|
31
|
+
// Note: LocalStorageProvider doesn't emit storage events on same tab write unless specific hook used.
|
|
32
|
+
// We can rely on a simpler polling or event listener if needed.
|
|
33
|
+
// For now, let's assume valid reload or Settings Page will signal us?
|
|
34
|
+
// In a real app, use a Context or Event Bus.
|
|
35
|
+
// Hack for immediate feedback from Settings Page:
|
|
36
|
+
window.addEventListener('meechi-appearance-update', async () => {
|
|
37
|
+
const config = await settingsManager.getConfig();
|
|
38
|
+
applyAppearance(config.appearance);
|
|
39
|
+
});
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener('meechi-appearance-update', () => { });
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
// Legacy Defaults to Ignore (Migration Hack)
|
|
45
|
+
const IGNORED_DEFAULTS = [
|
|
46
|
+
'#F9F7F2', // Old Light Background
|
|
47
|
+
'#FFFFFF', // Old Surface
|
|
48
|
+
'#1A1C1A', // Old Foreground
|
|
49
|
+
'#5C635C', // Old Secondary
|
|
50
|
+
'#6B8E6B', // Old Accent (Maybe keep this one? No, let's reset it to be safe if it matches default)
|
|
51
|
+
];
|
|
52
|
+
const applyAppearance = (appearance) => {
|
|
53
|
+
if (!appearance)
|
|
54
|
+
return;
|
|
55
|
+
const root = document.documentElement;
|
|
56
|
+
// Font Handling
|
|
57
|
+
if (appearance.fontFamily && !IGNORED_DEFAULTS.includes(appearance.fontFamily)) {
|
|
58
|
+
if (appearance.fontFamily.startsWith('http')) {
|
|
59
|
+
// ...
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (appearance.fontFamily === 'Inter') {
|
|
63
|
+
root.style.setProperty('--font-sans', 'var(--font-inter)');
|
|
64
|
+
}
|
|
65
|
+
else if (appearance.fontFamily === 'Lora') {
|
|
66
|
+
root.style.setProperty('--font-sans', 'var(--font-lora)');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
root.style.setProperty('--font-sans', 'monospace');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Accent Color (OKLCH Engine)
|
|
74
|
+
if (appearance.accentColor && !IGNORED_DEFAULTS.includes(appearance.accentColor)) {
|
|
75
|
+
const { l, c, h, cssValue } = getOklch(appearance.accentColor);
|
|
76
|
+
root.style.setProperty('--accent', cssValue);
|
|
77
|
+
// ... (rest of accent logic) ...
|
|
78
|
+
root.style.setProperty('--destructive', `oklch(${l} ${c} 25)`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Reset if it was previously set and now is ignored/empty
|
|
82
|
+
root.style.removeProperty('--accent');
|
|
83
|
+
root.style.removeProperty('--destructive');
|
|
84
|
+
}
|
|
85
|
+
// Backgrounds & Surfaces
|
|
86
|
+
// Helper to apply or clear
|
|
87
|
+
const setOrClear = (prop, value) => {
|
|
88
|
+
if (value && !IGNORED_DEFAULTS.includes(value)) {
|
|
89
|
+
root.style.setProperty(prop, value);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
root.style.removeProperty(prop);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
setOrClear('--background', appearance.backgroundColor);
|
|
96
|
+
setOrClear('--surface', appearance.surfaceColor);
|
|
97
|
+
setOrClear('--foreground', appearance.foregroundColor);
|
|
98
|
+
setOrClear('--secondary', appearance.secondaryColor);
|
|
99
|
+
};
|
|
100
|
+
if (!mounted) {
|
|
101
|
+
// Prevent hydration mismatch
|
|
102
|
+
return _jsx(_Fragment, { children: children });
|
|
103
|
+
}
|
|
104
|
+
return _jsx(NextThemesProvider, Object.assign({}, props, { children: children }));
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ThemeSwitcher(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import Icon from "@/components/Icon";
|
|
5
|
+
import { useTheme } from "next-themes";
|
|
6
|
+
export function ThemeSwitcher() {
|
|
7
|
+
const { setTheme, theme } = useTheme();
|
|
8
|
+
const [mounted, setMounted] = React.useState(false);
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
setMounted(true);
|
|
11
|
+
}, []);
|
|
12
|
+
if (!mounted) {
|
|
13
|
+
return (_jsx("div", { className: "flex items-center gap-2 p-1 bg-surface/10 rounded-full border border-border backdrop-blur-sm opacity-0", children: _jsx("div", { className: "w-8 h-8" }) }));
|
|
14
|
+
}
|
|
15
|
+
return (_jsxs("div", { className: "flex items-center gap-2 p-1 bg-surface/10 rounded-full border border-border backdrop-blur-sm", children: [_jsx("button", { onClick: () => setTheme("light"), className: `p-1.5 rounded-full transition-colors ${theme === 'light' ? 'bg-accent text-surface' : 'text-muted hover:text-accent'}`, title: "Light Mode", children: _jsx(Icon, { name: "Sun", size: 16 }) }), _jsx("button", { onClick: () => setTheme("system"), className: `p-1.5 rounded-full transition-colors ${theme === 'system' ? 'bg-accent text-surface' : 'text-muted hover:text-accent'}`, title: "System Mode", children: _jsx(Icon, { name: "Monitor", size: 16 }) }), _jsx("button", { onClick: () => setTheme("dark"), className: `p-1.5 rounded-full transition-colors ${theme === 'dark' ? 'bg-accent text-surface' : 'text-muted hover:text-accent'}`, title: "Dark Mode", children: _jsx(Icon, { name: "Moon", size: 16 }) })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface VoiceInputAreaProps {
|
|
2
|
+
meechi: any;
|
|
3
|
+
chatInput: string;
|
|
4
|
+
setChatInput: (val: string) => void;
|
|
5
|
+
isDragOver: boolean;
|
|
6
|
+
setIsDragOver: (val: boolean) => void;
|
|
7
|
+
attachedFiles: any[];
|
|
8
|
+
setAttachedFiles: (files: any[]) => void;
|
|
9
|
+
handleChat: (e: any) => void;
|
|
10
|
+
storage: any;
|
|
11
|
+
processUserMessage: (text: string, onToken?: (chunk: string) => void) => Promise<string | void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function VoiceInputArea({ meechi, chatInput, setChatInput, isDragOver, setIsDragOver, attachedFiles, setAttachedFiles, handleChat, storage, processUserMessage }: VoiceInputAreaProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { useVoiceLoop } from '../../lib/hooks/use-voice-loop';
|
|
4
|
+
import { TranscriberService } from '../../lib/audio/stt';
|
|
5
|
+
import { SynthesizerService } from '../../lib/audio/tts';
|
|
6
|
+
import { settingsManager } from '../../lib/settings';
|
|
7
|
+
import { Mic, X, Square, ArrowUp } from 'lucide-react';
|
|
8
|
+
import { motion } from 'framer-motion';
|
|
9
|
+
import Icon from '../../components/Icon';
|
|
10
|
+
import styles from '../../app/app/page.module.css';
|
|
11
|
+
export function VoiceInputArea({ meechi, chatInput, setChatInput, isDragOver, setIsDragOver, attachedFiles, setAttachedFiles, handleChat, storage, processUserMessage }) {
|
|
12
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
13
|
+
const { start, stop, stopPlayback, state, vadProb, transcript, getAnalyser, isPlaying } = useVoiceLoop(processUserMessage);
|
|
14
|
+
const [isVoiceActive, setIsVoiceActive] = useState(false);
|
|
15
|
+
const textareaRef = useRef(null);
|
|
16
|
+
// Auto-resize textarea
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const textarea = textareaRef.current;
|
|
19
|
+
if (textarea) {
|
|
20
|
+
textarea.style.height = 'auto';
|
|
21
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
|
22
|
+
}
|
|
23
|
+
}, [chatInput]);
|
|
24
|
+
// Handle Voice Toggle
|
|
25
|
+
const toggleVoice = () => {
|
|
26
|
+
console.log("[VoiceInputArea] Toggle clicked. Current state:", isVoiceActive);
|
|
27
|
+
if (isVoiceActive) {
|
|
28
|
+
stop();
|
|
29
|
+
stopPlayback(); // Cut speech immediately
|
|
30
|
+
setIsVoiceActive(false);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.log("[VoiceInputArea] Starting voice loop...");
|
|
34
|
+
start().then(() => {
|
|
35
|
+
console.log("[VoiceInputArea] Voice loop started successfully");
|
|
36
|
+
setIsVoiceActive(true);
|
|
37
|
+
}).catch(e => {
|
|
38
|
+
console.error("[VoiceInputArea] Failed to start voice loop:", e);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
console.log("[VoiceInputArea] Component Mounted - Preloading Models...");
|
|
44
|
+
const preload = async () => {
|
|
45
|
+
// 1. Configure Cloud STT if available
|
|
46
|
+
const config = await settingsManager.getConfig();
|
|
47
|
+
// Check Active Provider
|
|
48
|
+
if (config.activeProviderId && config.activeProviderId !== 'local') {
|
|
49
|
+
const provider = config.providers.find(p => p.id === config.activeProviderId);
|
|
50
|
+
const key = (provider === null || provider === void 0 ? void 0 : provider.apiKey) || process.env.NEXT_PUBLIC_GROQ_API_KEY;
|
|
51
|
+
if (key) {
|
|
52
|
+
console.log("[VoiceInputArea] Enabling Cloud STT & TTS (Groq/Cloud)");
|
|
53
|
+
TranscriberService.setCloudProvider(key);
|
|
54
|
+
SynthesizerService.setCloudProvider(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 2. Preload/Init TTS
|
|
58
|
+
// If Cloud TTS is configured (via setCloudProvider above), init() will defer local model.
|
|
59
|
+
SynthesizerService.init().catch(e => console.error("TTS Preload Failed", e));
|
|
60
|
+
};
|
|
61
|
+
setTimeout(preload, 1000);
|
|
62
|
+
}, []);
|
|
63
|
+
// Stop voice if component unmounts
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
return () => { stop(); };
|
|
66
|
+
}, []);
|
|
67
|
+
// Visualizer Data
|
|
68
|
+
const [volume, setVolume] = useState(0);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!isVoiceActive)
|
|
71
|
+
return;
|
|
72
|
+
let rafId;
|
|
73
|
+
const animate = () => {
|
|
74
|
+
const analyser = getAnalyser();
|
|
75
|
+
if (analyser) {
|
|
76
|
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
77
|
+
analyser.getByteFrequencyData(dataArray);
|
|
78
|
+
let sum = 0;
|
|
79
|
+
for (let i = 0; i < dataArray.length; i++)
|
|
80
|
+
sum += dataArray[i];
|
|
81
|
+
setVolume(sum / dataArray.length);
|
|
82
|
+
}
|
|
83
|
+
rafId = requestAnimationFrame(animate);
|
|
84
|
+
};
|
|
85
|
+
animate();
|
|
86
|
+
return () => cancelAnimationFrame(rafId);
|
|
87
|
+
}, [isVoiceActive, getAnalyser]);
|
|
88
|
+
const visualizerScale = 1 + (volume / 255) * 0.4 + (vadProb * 0.1);
|
|
89
|
+
const visualizerColor = state === 'listening' ? 'var(--accent)' :
|
|
90
|
+
state === 'processing' ? '#818cf8' :
|
|
91
|
+
state === 'speaking' ? '#34d399' : '#a8a29e';
|
|
92
|
+
return (_jsxs("form", { onSubmit: handleChat, className: styles.inputWrapper, style: { flexDirection: 'column', alignItems: 'stretch', position: 'relative' }, children: [isVoiceActive && (_jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, className: styles.voiceOverlayInline, style: {
|
|
93
|
+
position: 'absolute',
|
|
94
|
+
top: 0, left: 0, right: 0, bottom: 0,
|
|
95
|
+
background: 'rgba(255,255,255,0.85)',
|
|
96
|
+
backdropFilter: 'blur(4px)',
|
|
97
|
+
zIndex: 20,
|
|
98
|
+
borderRadius: 12,
|
|
99
|
+
display: 'flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
flexDirection: 'column',
|
|
103
|
+
gap: 8,
|
|
104
|
+
border: `1px solid ${visualizerColor}`
|
|
105
|
+
}, children: [_jsx(motion.div, { animate: { scale: visualizerScale, backgroundColor: visualizerColor }, style: { width: 40, height: 40, borderRadius: '50%', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' } }), _jsxs("div", { style: { fontSize: '0.85rem', color: '#555', fontStyle: 'italic' }, children: [state === 'idle' && "Listening...", state === 'listening' && "Listening...", state === 'processing' && "Thinking...", state === 'speaking' && "Speaking..."] }), transcript && _jsxs("div", { style: { fontSize: '0.75rem', maxWidth: '80%', textAlign: 'center', opacity: 0.7 }, children: ["\"", transcript, "\""] }), _jsx("button", { type: "button", onClick: toggleVoice, style: { position: 'absolute', top: 8, right: 8, background: 'none', border: 'none', cursor: 'pointer', color: '#666' }, children: _jsx(X, { size: 16 }) })] })), isDragOver && (_jsx("div", { style: {
|
|
106
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
107
|
+
background: 'rgba(59, 130, 246, 0.1)',
|
|
108
|
+
border: '2px dashed #3b82f6',
|
|
109
|
+
borderRadius: 8,
|
|
110
|
+
pointerEvents: 'none',
|
|
111
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
112
|
+
color: '#3b82f6', fontWeight: 600,
|
|
113
|
+
zIndex: 10
|
|
114
|
+
}, children: "Drop files here to attach" })), attachedFiles.length > 0 && (_jsx("div", { style: { width: '100%', padding: '0 0.5rem 0.5rem 0.5rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap', borderBottom: '1px solid rgba(255,255,255,0.05)', marginBottom: '0.5rem' }, children: attachedFiles.map((f, i) => (_jsx("div", { style: { fontSize: '0.75rem', background: '#e0f2fe', color: '#0369a1', padding: '2px 6px', borderRadius: 4 }, children: f.name }, i))) })), _jsxs("div", { style: { display: 'flex', width: '100%', alignItems: 'flex-end', gap: '0.5rem', flexDirection: 'column' }, children: [_jsx("div", { style: {
|
|
115
|
+
alignSelf: 'flex-start',
|
|
116
|
+
display: 'flex',
|
|
117
|
+
gap: '0.25rem',
|
|
118
|
+
padding: '2px',
|
|
119
|
+
background: 'var(--surface)',
|
|
120
|
+
borderRadius: 8,
|
|
121
|
+
marginBottom: 4,
|
|
122
|
+
border: '1px solid var(--border)'
|
|
123
|
+
}, children: ['chat', 'research'].filter(m => {
|
|
124
|
+
var _a;
|
|
125
|
+
// In Core, only show 'chat' and 'research' if the mode is active
|
|
126
|
+
// The list of available modes can be extended by the parent application via props
|
|
127
|
+
return ((_a = meechi.activeMemories) === null || _a === void 0 ? void 0 : _a.includes(m)) || m === 'chat';
|
|
128
|
+
}).map((m) => (_jsxs("button", { type: "button", onClick: () => meechi.setMode(m), style: {
|
|
129
|
+
background: meechi.mode === m ? 'var(--accent)' : 'transparent',
|
|
130
|
+
color: meechi.mode === m ? 'var(--background)' : 'var(--secondary)',
|
|
131
|
+
border: 'none',
|
|
132
|
+
borderRadius: 6,
|
|
133
|
+
padding: '4px 8px',
|
|
134
|
+
fontSize: '0.75rem',
|
|
135
|
+
cursor: 'pointer',
|
|
136
|
+
transition: 'all 0.2s ease',
|
|
137
|
+
display: 'flex', alignItems: 'center', gap: 4
|
|
138
|
+
}, children: [_jsx(Icon, { name: m === 'chat' ? "MessageCircle" : "Search", size: 14 }), m.charAt(0).toUpperCase() + m.slice(1)] }, m))) }), _jsxs("div", { style: { display: 'flex', width: '100%', alignItems: 'flex-end', gap: '0.5rem' }, children: [_jsx("textarea", { ref: textareaRef, value: chatInput, onChange: (e) => setChatInput(e.target.value), placeholder: meechi.mode === 'log' ? "Write to your Ship's Log..." :
|
|
139
|
+
meechi.mode === 'research' ? "Ask a grounded question (Strict RAG)..." :
|
|
140
|
+
"Ask Meechi anything... (Creative)", onDragEnter: () => setIsDragOver(true), onDragLeave: () => setIsDragOver(false), onDragOver: (e) => { e.preventDefault(); setIsDragOver(true); }, onDrop: async (e) => {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
setIsDragOver(false);
|
|
143
|
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
144
|
+
const files = Array.from(e.dataTransfer.files);
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const safeName = file.name.replace(/[^a-zA-Z0-9.\-_ ()]/g, '').replace(/\s+/g, ' ').trim();
|
|
147
|
+
const path = `temp/${safeName}`;
|
|
148
|
+
if (file.type.startsWith('text/') || file.name.endsWith('.md')) {
|
|
149
|
+
const text = await file.text();
|
|
150
|
+
await storage.saveFile(path, text);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const buffer = await file.arrayBuffer();
|
|
154
|
+
await storage.saveFile(path, buffer);
|
|
155
|
+
}
|
|
156
|
+
setAttachedFiles([...attachedFiles, { name: safeName, path }]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}, onKeyDown: (e) => {
|
|
160
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
handleChat(e);
|
|
163
|
+
}
|
|
164
|
+
}, className: styles.chatInput, rows: 1, style: { width: '100%' }, disabled: isVoiceActive }), _jsx("button", { type: "button", onClick: () => {
|
|
165
|
+
var _a, _b;
|
|
166
|
+
const hasVoiceMsg = ((_a = meechi.activeMemories) === null || _a === void 0 ? void 0 : _a.includes('groq-voice')) || ((_b = meechi.activeMemories) === null || _b === void 0 ? void 0 : _b.includes('local-voice'));
|
|
167
|
+
if (hasVoiceMsg)
|
|
168
|
+
toggleVoice();
|
|
169
|
+
}, style: {
|
|
170
|
+
background: isVoiceActive ? 'var(--accent)' : 'transparent',
|
|
171
|
+
color: isVoiceActive ? 'white' : 'var(--secondary)',
|
|
172
|
+
border: 'none',
|
|
173
|
+
cursor: (((_a = meechi.activeMemories) === null || _a === void 0 ? void 0 : _a.includes('groq-voice')) || ((_b = meechi.activeMemories) === null || _b === void 0 ? void 0 : _b.includes('local-voice'))) ? 'pointer' : 'default',
|
|
174
|
+
padding: '8px',
|
|
175
|
+
borderRadius: '50%',
|
|
176
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
177
|
+
opacity: isVoiceActive ? 1 : (((_c = meechi.activeMemories) === null || _c === void 0 ? void 0 : _c.includes('groq-voice')) || ((_d = meechi.activeMemories) === null || _d === void 0 ? void 0 : _d.includes('local-voice')) ? 0.6 : 0.4),
|
|
178
|
+
transition: 'all 0.2s ease',
|
|
179
|
+
pointerEvents: (((_e = meechi.activeMemories) === null || _e === void 0 ? void 0 : _e.includes('groq-voice')) || ((_f = meechi.activeMemories) === null || _f === void 0 ? void 0 : _f.includes('local-voice'))) ? 'auto' : 'none'
|
|
180
|
+
}, title: (((_g = meechi.activeMemories) === null || _g === void 0 ? void 0 : _g.includes('groq-voice')) || ((_h = meechi.activeMemories) === null || _h === void 0 ? void 0 : _h.includes('local-voice'))) ? "Toggle Voice Mode" : "Voice MCP not active", children: _jsx(Mic, { size: 20 }) }), (((_j = meechi.localAIStatus) === null || _j === void 0 ? void 0 : _j.includes('Generating')) || ((_k = meechi.localAIStatus) === null || _k === void 0 ? void 0 : _k.includes('Thinking')) || state === 'speaking' || isPlaying || (state === 'processing')) ? (_jsx("button", { type: "button", onClick: (e) => {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
if (isVoiceActive) {
|
|
184
|
+
stopPlayback(); // Stop Audio
|
|
185
|
+
}
|
|
186
|
+
meechi.stop(); // Stop LLM
|
|
187
|
+
}, className: styles.sendBtn, style: {
|
|
188
|
+
background: 'var(--destructive)', color: 'white'
|
|
189
|
+
}, children: _jsx(Square, { size: 16, fill: "currentColor" }) })) : (_jsx("button", { type: "submit", className: styles.sendBtn, style: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx(ArrowUp, { size: 20 }) }))] })] })] }));
|
|
190
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface VoiceOverlayProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
sendMessage: (text: string) => Promise<string | void | undefined>;
|
|
5
|
+
}
|
|
6
|
+
export declare function VoiceOverlay({ isOpen, onClose, sendMessage }: VoiceOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { X, Mic, MicOff } from "lucide-react";
|
|
5
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
6
|
+
import { useVoiceLoop } from "@/lib/hooks/use-voice-loop";
|
|
7
|
+
export function VoiceOverlay({ isOpen, onClose, sendMessage }) {
|
|
8
|
+
const { start, stop, state, vadProb, transcript, getAnalyser } = useVoiceLoop(sendMessage);
|
|
9
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
10
|
+
const [volume, setVolume] = useState(0);
|
|
11
|
+
// Animation Loop for Volume
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!isOpen)
|
|
14
|
+
return;
|
|
15
|
+
let rafId;
|
|
16
|
+
const analyser = getAnalyser();
|
|
17
|
+
const bufferLength = analyser ? analyser.frequencyBinCount : 0;
|
|
18
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
19
|
+
const animate = () => {
|
|
20
|
+
if (!analyser) {
|
|
21
|
+
// Retry if analyser not ready (usually is after start())
|
|
22
|
+
const retry = getAnalyser();
|
|
23
|
+
if (retry) {
|
|
24
|
+
// recursive restart with ready analyser
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
analyser.getByteFrequencyData(dataArray);
|
|
29
|
+
// Calculate average volume
|
|
30
|
+
let sum = 0;
|
|
31
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
32
|
+
sum += dataArray[i];
|
|
33
|
+
}
|
|
34
|
+
const avg = sum / bufferLength;
|
|
35
|
+
setVolume(avg); // 0-255
|
|
36
|
+
}
|
|
37
|
+
rafId = requestAnimationFrame(animate);
|
|
38
|
+
};
|
|
39
|
+
animate();
|
|
40
|
+
return () => cancelAnimationFrame(rafId);
|
|
41
|
+
}, [isOpen, state, getAnalyser]); // Re-run if state changes or analyser becomes available
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isOpen) {
|
|
44
|
+
start();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
stop();
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
stop();
|
|
51
|
+
};
|
|
52
|
+
}, [isOpen]);
|
|
53
|
+
const toggleMute = () => {
|
|
54
|
+
if (isMuted) {
|
|
55
|
+
// Resume
|
|
56
|
+
start();
|
|
57
|
+
setIsMuted(false);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
stop(); // Stop listening
|
|
61
|
+
setIsMuted(true);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
// Visualizer scale based on Volume (0-255) + small VAD bump
|
|
65
|
+
// Base scale 1.0, max ~1.5
|
|
66
|
+
const visualizerScale = 1 + (volume / 255) * 0.6 + (vadProb * 0.1);
|
|
67
|
+
const visualizerColor = state === 'listening' ? 'bg-sage-500' :
|
|
68
|
+
state === 'processing' ? 'bg-indigo-300' :
|
|
69
|
+
state === 'speaking' ? 'bg-emerald-400' : 'bg-stone-300';
|
|
70
|
+
return (_jsx(AnimatePresence, { children: isOpen && (_jsxs(motion.div, { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 20 }, className: "fixed inset-0 z-50 flex flex-col items-center justify-center bg-paper-100/95 backdrop-blur-xl", children: [_jsx("div", { className: "absolute top-6 right-6", children: _jsx("button", { onClick: onClose, className: "p-4 rounded-full bg-stone-200 text-stone-600 hover:bg-stone-300 transition-colors", children: _jsx(X, { size: 24 }) }) }), _jsxs("div", { className: "relative flex items-center justify-center w-64 h-64", children: [_jsx(motion.div, { animate: { scale: state === 'listening' ? [1, 1.2, 1] : 1 }, transition: { repeat: Infinity, duration: 2 }, className: `absolute inset-0 rounded-full opacity-20 ${visualizerColor}` }), _jsx(motion.div, { animate: { scale: visualizerScale }, className: `w-32 h-32 rounded-full shadow-2xl transition-colors duration-300 ${visualizerColor}` }), _jsxs("div", { className: "absolute -bottom-16 text-stone-500 font-serif text-lg tracking-wide", children: [state === 'idle' && "Listening...", state === 'listening' && "I'm listening...", state === 'processing' && "Thinking...", state === 'speaking' && "Speaking..."] })] }), _jsx("div", { className: "mt-24 h-24 px-8 text-center max-w-2xl", children: _jsx(AnimatePresence, { mode: "wait", children: transcript && (_jsxs(motion.p, { initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, className: "text-2xl font-serif text-stone-800 leading-relaxed", children: ["\"", transcript, "\""] }, transcript)) }) }), _jsx("div", { className: "absolute bottom-12 flex gap-8", children: _jsx("button", { onClick: toggleMute, className: `p-6 rounded-full transition-all duration-300 ${isMuted ? 'bg-red-100 text-red-600' : 'bg-white text-stone-800 shadow-lg'}`, children: isMuted ? _jsx(MicOff, { size: 32 }) : _jsx(Mic, { size: 32 }) }) })] })) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AIChatMessage } from '../lib/ai/types';
|
|
2
|
+
export declare function useMeechi(): {
|
|
3
|
+
isReady: boolean;
|
|
4
|
+
localAIStatus: string;
|
|
5
|
+
downloadProgress: {
|
|
6
|
+
percentage: number;
|
|
7
|
+
text: string;
|
|
8
|
+
} | null;
|
|
9
|
+
chat: (userMsg: string, history: AIChatMessage[], context: string, onUpdate: (chunk: string) => void, onToolStart?: (toolName: string) => void, onToolResult?: (result: string) => void) => Promise<void>;
|
|
10
|
+
isLowPowerDevice: boolean;
|
|
11
|
+
loadedModel: string | null;
|
|
12
|
+
activeMemories: string[];
|
|
13
|
+
mode: string;
|
|
14
|
+
setMode: (m: string) => void;
|
|
15
|
+
stop: () => Promise<void>;
|
|
16
|
+
};
|