@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.
Files changed (116) hide show
  1. package/LICENSE +624 -0
  2. package/README.md +59 -0
  3. package/dist/components/CalendarView.d.ts +3 -0
  4. package/dist/components/CalendarView.js +72 -0
  5. package/dist/components/ChatInterface.d.ts +6 -0
  6. package/dist/components/ChatInterface.js +105 -0
  7. package/dist/components/FileExplorer.d.ts +9 -0
  8. package/dist/components/FileExplorer.js +757 -0
  9. package/dist/components/Icon.d.ts +9 -0
  10. package/dist/components/Icon.js +44 -0
  11. package/dist/components/SourceEditor.d.ts +13 -0
  12. package/dist/components/SourceEditor.js +50 -0
  13. package/dist/components/ThemeProvider.d.ts +5 -0
  14. package/dist/components/ThemeProvider.js +105 -0
  15. package/dist/components/ThemeSwitcher.d.ts +1 -0
  16. package/dist/components/ThemeSwitcher.js +16 -0
  17. package/dist/components/voice/VoiceInputArea.d.ts +14 -0
  18. package/dist/components/voice/VoiceInputArea.js +190 -0
  19. package/dist/components/voice/VoiceOverlay.d.ts +7 -0
  20. package/dist/components/voice/VoiceOverlay.js +71 -0
  21. package/dist/hooks/useMeechi.d.ts +16 -0
  22. package/dist/hooks/useMeechi.js +461 -0
  23. package/dist/hooks/useSync.d.ts +8 -0
  24. package/dist/hooks/useSync.js +87 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.js +22 -0
  27. package/dist/lib/ai/embeddings.d.ts +15 -0
  28. package/dist/lib/ai/embeddings.js +128 -0
  29. package/dist/lib/ai/gpu-lock.d.ts +19 -0
  30. package/dist/lib/ai/gpu-lock.js +43 -0
  31. package/dist/lib/ai/llm.worker.d.ts +1 -0
  32. package/dist/lib/ai/llm.worker.js +7 -0
  33. package/dist/lib/ai/local-llm.d.ts +30 -0
  34. package/dist/lib/ai/local-llm.js +211 -0
  35. package/dist/lib/ai/manager.d.ts +20 -0
  36. package/dist/lib/ai/manager.js +51 -0
  37. package/dist/lib/ai/parsing.d.ts +12 -0
  38. package/dist/lib/ai/parsing.js +56 -0
  39. package/dist/lib/ai/prompts.d.ts +2 -0
  40. package/dist/lib/ai/prompts.js +2 -0
  41. package/dist/lib/ai/providers/gemini.d.ts +6 -0
  42. package/dist/lib/ai/providers/gemini.js +88 -0
  43. package/dist/lib/ai/providers/groq.d.ts +6 -0
  44. package/dist/lib/ai/providers/groq.js +42 -0
  45. package/dist/lib/ai/registry.d.ts +29 -0
  46. package/dist/lib/ai/registry.js +52 -0
  47. package/dist/lib/ai/tools.d.ts +2 -0
  48. package/dist/lib/ai/tools.js +106 -0
  49. package/dist/lib/ai/types.d.ts +22 -0
  50. package/dist/lib/ai/types.js +1 -0
  51. package/dist/lib/ai/worker.d.ts +1 -0
  52. package/dist/lib/ai/worker.js +60 -0
  53. package/dist/lib/audio/input.d.ts +13 -0
  54. package/dist/lib/audio/input.js +121 -0
  55. package/dist/lib/audio/stt.d.ts +13 -0
  56. package/dist/lib/audio/stt.js +119 -0
  57. package/dist/lib/audio/tts.d.ts +12 -0
  58. package/dist/lib/audio/tts.js +128 -0
  59. package/dist/lib/audio/vad.d.ts +18 -0
  60. package/dist/lib/audio/vad.js +117 -0
  61. package/dist/lib/colors.d.ts +16 -0
  62. package/dist/lib/colors.js +67 -0
  63. package/dist/lib/extensions.d.ts +35 -0
  64. package/dist/lib/extensions.js +24 -0
  65. package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
  66. package/dist/lib/hooks/use-voice-loop.js +313 -0
  67. package/dist/lib/mcp/McpClient.d.ts +19 -0
  68. package/dist/lib/mcp/McpClient.js +42 -0
  69. package/dist/lib/mcp/McpRegistry.d.ts +47 -0
  70. package/dist/lib/mcp/McpRegistry.js +117 -0
  71. package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
  72. package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
  73. package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
  74. package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
  75. package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
  76. package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
  77. package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
  78. package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
  79. package/dist/lib/mcp/native/index.d.ts +10 -0
  80. package/dist/lib/mcp/native/index.js +10 -0
  81. package/dist/lib/mcp/types.d.ts +35 -0
  82. package/dist/lib/mcp/types.js +1 -0
  83. package/dist/lib/pdf.d.ts +10 -0
  84. package/dist/lib/pdf.js +142 -0
  85. package/dist/lib/settings.d.ts +48 -0
  86. package/dist/lib/settings.js +87 -0
  87. package/dist/lib/storage/db.d.ts +57 -0
  88. package/dist/lib/storage/db.js +45 -0
  89. package/dist/lib/storage/local.d.ts +28 -0
  90. package/dist/lib/storage/local.js +534 -0
  91. package/dist/lib/storage/migrate.d.ts +3 -0
  92. package/dist/lib/storage/migrate.js +122 -0
  93. package/dist/lib/storage/types.d.ts +66 -0
  94. package/dist/lib/storage/types.js +1 -0
  95. package/dist/lib/sync/client-drive.d.ts +9 -0
  96. package/dist/lib/sync/client-drive.js +69 -0
  97. package/dist/lib/sync/engine.d.ts +18 -0
  98. package/dist/lib/sync/engine.js +517 -0
  99. package/dist/lib/sync/google-drive.d.ts +52 -0
  100. package/dist/lib/sync/google-drive.js +183 -0
  101. package/dist/lib/sync/merge.d.ts +1 -0
  102. package/dist/lib/sync/merge.js +68 -0
  103. package/dist/lib/yjs/YjsProvider.d.ts +11 -0
  104. package/dist/lib/yjs/YjsProvider.js +33 -0
  105. package/dist/lib/yjs/graph.d.ts +11 -0
  106. package/dist/lib/yjs/graph.js +7 -0
  107. package/dist/lib/yjs/hooks.d.ts +7 -0
  108. package/dist/lib/yjs/hooks.js +37 -0
  109. package/dist/lib/yjs/store.d.ts +4 -0
  110. package/dist/lib/yjs/store.js +19 -0
  111. package/dist/lib/yjs/syncGraph.d.ts +1 -0
  112. package/dist/lib/yjs/syncGraph.js +38 -0
  113. package/dist/providers/theme-provider.d.ts +3 -0
  114. package/dist/providers/theme-provider.js +18 -0
  115. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  116. 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
+ };