@mazees/mark 5.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 +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { getAllConfig, saveConfiguration } from '../api/db'
|
|
3
|
+
import { getExtractor } from '../api/vectorMemory'
|
|
4
|
+
import { driver } from 'driver.js'
|
|
5
|
+
import 'driver.js/dist/driver.css'
|
|
6
|
+
import { useConfirm } from '../hooks/useConfirm'
|
|
7
|
+
import { useChat } from '../contexts/ChatContext'
|
|
8
|
+
import {
|
|
9
|
+
AiEngineSection,
|
|
10
|
+
CameraSection,
|
|
11
|
+
ShortcutSection,
|
|
12
|
+
VoiceSection,
|
|
13
|
+
PermissionsSection
|
|
14
|
+
} from '../components/config'
|
|
15
|
+
|
|
16
|
+
const Configuration = ({ isFirstSetup = false, onSetupComplete = null }) => {
|
|
17
|
+
const [config, setConfig] = useState({
|
|
18
|
+
personality: 'Santai layaknya seorang teman dan suka bercanda.',
|
|
19
|
+
model: 'local-model',
|
|
20
|
+
customModel: 'default-model',
|
|
21
|
+
temperature: 0,
|
|
22
|
+
ttsRate: 10,
|
|
23
|
+
ttsPitch: 55,
|
|
24
|
+
aiProvider: 'gemini-web',
|
|
25
|
+
geminiWebModel: 'gemini-3.6-flash',
|
|
26
|
+
deepseekWebModel: 'deepseek-chat',
|
|
27
|
+
deepseekUserToken: '',
|
|
28
|
+
wakeWordEnabled: true,
|
|
29
|
+
customWakeWords: '',
|
|
30
|
+
speechLanguage: 'id-ID',
|
|
31
|
+
tgBotToken: '',
|
|
32
|
+
tgAdminIds: '',
|
|
33
|
+
awarenessEnabled: true,
|
|
34
|
+
cameraDeviceId: 'default',
|
|
35
|
+
cameraEnabled: true,
|
|
36
|
+
bgOverlayOpacity: 65
|
|
37
|
+
})
|
|
38
|
+
const [videoDevices, setVideoDevices] = useState([])
|
|
39
|
+
const [loading, setLoading] = useState(true)
|
|
40
|
+
const [isDownloadingModel, setIsDownloadingModel] = useState(false)
|
|
41
|
+
const [downloadProgress, setDownloadProgress] = useState(0)
|
|
42
|
+
const { ModalComponent } = useConfirm()
|
|
43
|
+
const chatContext = useChat()
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let isMounted = true
|
|
47
|
+
const init = async () => {
|
|
48
|
+
try {
|
|
49
|
+
setLoading(true)
|
|
50
|
+
await loadConfig()
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error('Error loading config:', err)
|
|
53
|
+
} finally {
|
|
54
|
+
if (isMounted) setLoading(false)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
init()
|
|
58
|
+
|
|
59
|
+
navigator.mediaDevices
|
|
60
|
+
.getUserMedia({ video: true })
|
|
61
|
+
.then((stream) => {
|
|
62
|
+
navigator.mediaDevices
|
|
63
|
+
.enumerateDevices()
|
|
64
|
+
.then((devices) => {
|
|
65
|
+
const cameras = devices.filter((d) => d.kind === 'videoinput')
|
|
66
|
+
if (isMounted) setVideoDevices(cameras)
|
|
67
|
+
})
|
|
68
|
+
.catch((err) => console.error('Error enumerating devices', err))
|
|
69
|
+
|
|
70
|
+
// Stop stream immediately since we just needed permission
|
|
71
|
+
stream.getTracks().forEach((track) => track.stop())
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => console.error('Cam permission denied', err))
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
isMounted = false
|
|
77
|
+
}
|
|
78
|
+
}, [])
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (isFirstSetup) {
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
const driverObj = driver({
|
|
84
|
+
showProgress: true,
|
|
85
|
+
animate: true,
|
|
86
|
+
nextBtnText: 'Lanjut',
|
|
87
|
+
prevBtnText: 'Kembali',
|
|
88
|
+
doneBtnText: 'Paham!',
|
|
89
|
+
steps: [
|
|
90
|
+
{
|
|
91
|
+
popover: {
|
|
92
|
+
title: 'Halo, Selamat Datang di Mark! 👋',
|
|
93
|
+
description:
|
|
94
|
+
'Mark adalah asisten AI pribadimu. Sebelum mulai ngobrol, ayo kita kenalan dulu sama pengaturan utamanya biar Mark bisa kerja maksimal buat kamu!',
|
|
95
|
+
side: 'top',
|
|
96
|
+
align: 'center'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
element: '#tour-ai-provider',
|
|
101
|
+
popover: {
|
|
102
|
+
title: '1. Pilih Mesin AI',
|
|
103
|
+
description:
|
|
104
|
+
'Kamu bisa milih mau pakai Gemini Gratis tanpa API Key, AI lokal (LM Studio), atau Custom API OpenAI-Compatible.',
|
|
105
|
+
side: 'bottom',
|
|
106
|
+
align: 'start'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
element: '#tour-embed-provider',
|
|
111
|
+
popover: {
|
|
112
|
+
title: '2. Memori AI',
|
|
113
|
+
description:
|
|
114
|
+
'Ini otak tempat Mark mengingat semuanya. Berjalan 100% lokal dengan vector embeddings tanpa setup tambahan.',
|
|
115
|
+
side: 'top',
|
|
116
|
+
align: 'start'
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
element: '#tour-wakeword',
|
|
121
|
+
popover: {
|
|
122
|
+
title: '3. Wake Word & Voice',
|
|
123
|
+
description:
|
|
124
|
+
'Mark mendukung deteksi suara "Hey Mark / Mark" otomatis via Web Speech API bawaan Microsoft Edge / Chrome.',
|
|
125
|
+
side: 'top',
|
|
126
|
+
align: 'start'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
element: '#tour-persona',
|
|
131
|
+
popover: {
|
|
132
|
+
title: '4. Kepribadian Mark',
|
|
133
|
+
description:
|
|
134
|
+
'Di sini kamu bebas nentuin gaya bicara Mark. Mau dia formal kayak asisten pro, atau santai kayak temen nongkrong? Tulis aja di sini!',
|
|
135
|
+
side: 'top',
|
|
136
|
+
align: 'start'
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
element: '#tour-temperature',
|
|
141
|
+
popover: {
|
|
142
|
+
title: '5. Kreativitas AI',
|
|
143
|
+
description:
|
|
144
|
+
'Temperature nentuin seberapa kreatif Mark. Angka kecil (0-0.3) bikin dia kaku tapi akurat, angka besar (0.7-1.0) bikin dia imajinatif dan luwes.',
|
|
145
|
+
side: 'top',
|
|
146
|
+
align: 'start'
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
element: '#tour-tts',
|
|
151
|
+
popover: {
|
|
152
|
+
title: '6. Pengaturan Suara',
|
|
153
|
+
description:
|
|
154
|
+
'Atur kecepatan (Rate) dan tinggi-rendahnya nada suara (Pitch) Mark. Kamu bisa klik "Test Suara Mark" buat dengerin hasil racikanmu!',
|
|
155
|
+
side: 'top',
|
|
156
|
+
align: 'start'
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
element: '#tour-save-btn',
|
|
161
|
+
popover: {
|
|
162
|
+
title: 'Simpan & Mulai',
|
|
163
|
+
description:
|
|
164
|
+
'Kalau udah diisi semua (termasuk API key kalau pakai Cloud), klik di sini buat mulai ngobrol sama Mark!',
|
|
165
|
+
side: 'top',
|
|
166
|
+
align: 'center'
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
})
|
|
171
|
+
driverObj.drive()
|
|
172
|
+
}, 500)
|
|
173
|
+
}
|
|
174
|
+
}, [isFirstSetup])
|
|
175
|
+
|
|
176
|
+
const loadConfig = async () => {
|
|
177
|
+
const data = await getAllConfig()
|
|
178
|
+
if (data.length > 0) {
|
|
179
|
+
setConfig((prev) => ({
|
|
180
|
+
...prev,
|
|
181
|
+
...data[0],
|
|
182
|
+
aiProvider: data[0].aiProvider || 'gemini-web',
|
|
183
|
+
geminiWebModel: data[0].geminiWebModel || 'gemini-3.6-flash',
|
|
184
|
+
deepseekWebModel: data[0].deepseekWebModel || 'deepseek-chat',
|
|
185
|
+
deepseekUserToken: data[0].deepseekUserToken || '',
|
|
186
|
+
micDeviceId: data[0].micDeviceId || 'default',
|
|
187
|
+
awarenessEnabled: data[0].awarenessEnabled ?? true,
|
|
188
|
+
bgOverlayOpacity:
|
|
189
|
+
data[0].bgOverlayOpacity !== undefined ? Number(data[0].bgOverlayOpacity) : 65
|
|
190
|
+
}))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handleSaveConfiguration = async () => {
|
|
195
|
+
if (config.aiProvider === 'custom') {
|
|
196
|
+
const endpoint = config.customEndpoint?.trim() || ''
|
|
197
|
+
if (!endpoint.endsWith('/chat/completions')) {
|
|
198
|
+
alert(
|
|
199
|
+
'Gagal Menyimpan: Custom Endpoint URL tidak valid! URL wajib diakhiri dengan /chat/completions (Contoh: https://api.openai.com/v1/chat/completions).'
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setIsDownloadingModel(true)
|
|
206
|
+
setDownloadProgress(0)
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
let extStats = {}
|
|
210
|
+
await getExtractor((info) => {
|
|
211
|
+
if (info.status === 'initiate') {
|
|
212
|
+
extStats[info.file] = { loaded: 0, total: info.total || 0 }
|
|
213
|
+
} else if (info.status === 'progress') {
|
|
214
|
+
if (extStats[info.file]) {
|
|
215
|
+
extStats[info.file].loaded = info.loaded
|
|
216
|
+
extStats[info.file].total = info.total
|
|
217
|
+
}
|
|
218
|
+
const values = Object.values(extStats)
|
|
219
|
+
const totalBytes = values.reduce((acc, curr) => acc + curr.total, 0)
|
|
220
|
+
const loadedBytes = values.reduce((acc, curr) => acc + curr.loaded, 0)
|
|
221
|
+
if (totalBytes > 0) {
|
|
222
|
+
setDownloadProgress(Math.round((loadedBytes / totalBytes) * 100))
|
|
223
|
+
}
|
|
224
|
+
} else if (info.status === 'done' || info.status === 'ready') {
|
|
225
|
+
setDownloadProgress(100)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error(e)
|
|
230
|
+
}
|
|
231
|
+
setIsDownloadingModel(false)
|
|
232
|
+
await saveConfiguration(config)
|
|
233
|
+
|
|
234
|
+
// Update global state without reloading the page
|
|
235
|
+
if (chatContext && chatContext.setConfig) {
|
|
236
|
+
chatContext.setConfig([config])
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isFirstSetup && onSetupComplete) {
|
|
240
|
+
onSetupComplete()
|
|
241
|
+
} else {
|
|
242
|
+
window.location.href = '#/'
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const handleBack = () => window.history.back()
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div className="h-screen text-white overflow-hidden relative font-['Poppins',sans-serif] bg-base-300 rounded-xl border border-white/5 shadow-2xl">
|
|
250
|
+
{/* Background Ambience */}
|
|
251
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,oklch(var(--n))_0%,transparent_70%)] opacity-20 pointer-events-none" />
|
|
252
|
+
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-10 pointer-events-none" />
|
|
253
|
+
|
|
254
|
+
{/* Main Content Area */}
|
|
255
|
+
<div className="relative z-10 w-full h-full overflow-y-auto custom-scrollbar">
|
|
256
|
+
<div className="max-w-2xl mx-auto px-4 py-8 pb-32 space-y-8">
|
|
257
|
+
{/* Page Header */}
|
|
258
|
+
<div className="flex items-center gap-4">
|
|
259
|
+
{!isFirstSetup && (
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={handleBack}
|
|
263
|
+
className="btn btn-ghost btn-sm btn-circle shrink-0"
|
|
264
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
265
|
+
title="Kembali"
|
|
266
|
+
>
|
|
267
|
+
<svg
|
|
268
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
269
|
+
width="1.2em"
|
|
270
|
+
height="1.2em"
|
|
271
|
+
fill="none"
|
|
272
|
+
viewBox="0 0 24 24"
|
|
273
|
+
stroke="currentColor"
|
|
274
|
+
strokeWidth={2}
|
|
275
|
+
>
|
|
276
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
277
|
+
</svg>
|
|
278
|
+
</button>
|
|
279
|
+
)}
|
|
280
|
+
<div>
|
|
281
|
+
<h1 className="text-2xl font-bold">
|
|
282
|
+
{isFirstSetup ? 'Selamat Datang di Mark!' : 'Pengaturan Mark'}
|
|
283
|
+
</h1>
|
|
284
|
+
<p className="opacity-50 text-sm mt-1">
|
|
285
|
+
{isFirstSetup
|
|
286
|
+
? 'Sebelum mulai ngobrol, atur provider AI dan pengaturan dasar lainnya di bawah ini.'
|
|
287
|
+
: 'Sesuaikan perilaku Mark dengan preferensimu.'}
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
{loading ? (
|
|
293
|
+
<div className="flex-1 min-h-[50vh] flex flex-col items-center justify-center gap-3">
|
|
294
|
+
<span className="loading loading-spinner loading-lg text-primary" />
|
|
295
|
+
<p className="font-mono text-xs text-base-content/50 tracking-widest uppercase">
|
|
296
|
+
Memuat Pengaturan Mark...
|
|
297
|
+
</p>
|
|
298
|
+
</div>
|
|
299
|
+
) : (
|
|
300
|
+
<>
|
|
301
|
+
{/* 1. AI Engine & Tools */}
|
|
302
|
+
<AiEngineSection config={config} setConfig={setConfig} />
|
|
303
|
+
|
|
304
|
+
<div className="divider"></div>
|
|
305
|
+
|
|
306
|
+
{/* 2. Camera Settings */}
|
|
307
|
+
<CameraSection config={config} setConfig={setConfig} videoDevices={videoDevices} />
|
|
308
|
+
|
|
309
|
+
{/* 3. Global Shortcut Settings */}
|
|
310
|
+
<ShortcutSection config={config} setConfig={setConfig} />
|
|
311
|
+
|
|
312
|
+
<div className="divider"></div>
|
|
313
|
+
|
|
314
|
+
{/* 4. Voice & Wake Word Settings */}
|
|
315
|
+
<VoiceSection config={config} setConfig={setConfig} />
|
|
316
|
+
|
|
317
|
+
<div className="divider"></div>
|
|
318
|
+
|
|
319
|
+
{/* 6. Security & Whitelist Permissions */}
|
|
320
|
+
<PermissionsSection />
|
|
321
|
+
|
|
322
|
+
{/* 7. Save Bar */}
|
|
323
|
+
<div className="flex flex-col items-end pt-2">
|
|
324
|
+
{isDownloadingModel && (
|
|
325
|
+
<div className="w-full max-w-xs mb-4">
|
|
326
|
+
<div className="flex justify-between text-xs mb-1">
|
|
327
|
+
<span>Mengunduh Model Embeddings...</span>
|
|
328
|
+
<span>{downloadProgress}%</span>
|
|
329
|
+
</div>
|
|
330
|
+
<progress
|
|
331
|
+
className="progress progress-primary w-full"
|
|
332
|
+
value={downloadProgress}
|
|
333
|
+
max="100"
|
|
334
|
+
></progress>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
<button
|
|
338
|
+
id="tour-save-btn"
|
|
339
|
+
onClick={handleSaveConfiguration}
|
|
340
|
+
disabled={isDownloadingModel}
|
|
341
|
+
className="btn btn-primary px-8 cursor-pointer"
|
|
342
|
+
>
|
|
343
|
+
{isDownloadingModel
|
|
344
|
+
? 'Menyimpan...'
|
|
345
|
+
: isFirstSetup
|
|
346
|
+
? 'Simpan & Mulai Gunakan Mark'
|
|
347
|
+
: 'Simpan Pengaturan'}
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<ModalComponent />
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export default Configuration
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { FaGoogle, FaKey, FaSave, FaCheckCircle, FaExclamationTriangle } from 'react-icons/fa'
|
|
4
|
+
import { getAllConfig, saveConfiguration } from '../api/db'
|
|
5
|
+
import { webApi } from '../api/web-bridge'
|
|
6
|
+
|
|
7
|
+
const GoogleWorkspace = () => {
|
|
8
|
+
const navigate = useNavigate()
|
|
9
|
+
const [config, setConfig] = useState({})
|
|
10
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
11
|
+
const [isSaving, setIsSaving] = useState(false)
|
|
12
|
+
const [toastMessage, setToastMessage] = useState(null)
|
|
13
|
+
const [isConnected, setIsConnected] = useState(false)
|
|
14
|
+
const [isConnecting, setIsConnecting] = useState(false)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
loadConfig()
|
|
18
|
+
checkConnectionStatus()
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
const checkConnectionStatus = async () => {
|
|
22
|
+
const res = await webApi.googleStatus()
|
|
23
|
+
setIsConnected(Boolean(res?.isConnected))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const handleConnect = async () => {
|
|
27
|
+
if (!config.googleClientId || !config.googleClientSecret) {
|
|
28
|
+
showToast('Client ID & Secret harus diisi dan disimpan dulu!')
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
setIsConnecting(true)
|
|
32
|
+
try {
|
|
33
|
+
const res = await webApi.googleConnect(config.googleClientId, config.googleClientSecret)
|
|
34
|
+
if (res.success) {
|
|
35
|
+
showToast('Berhasil terhubung ke Google Workspace!')
|
|
36
|
+
setIsConnected(true)
|
|
37
|
+
} else {
|
|
38
|
+
showToast(`Gagal: ${res.error}`)
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
showToast('Gagal memproses login.')
|
|
42
|
+
} finally {
|
|
43
|
+
setIsConnecting(false)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleDisconnect = async () => {
|
|
48
|
+
try {
|
|
49
|
+
await webApi.googleDisconnect()
|
|
50
|
+
setIsConnected(false)
|
|
51
|
+
showToast('Terputus dari Google Workspace.')
|
|
52
|
+
} catch (e) {
|
|
53
|
+
showToast('Gagal memutuskan koneksi.')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const loadConfig = async () => {
|
|
58
|
+
try {
|
|
59
|
+
const data = await getAllConfig()
|
|
60
|
+
if (data && data.length > 0) {
|
|
61
|
+
setConfig(data[0])
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to load config:', error)
|
|
65
|
+
} finally {
|
|
66
|
+
setIsLoading(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleSave = async () => {
|
|
71
|
+
setIsSaving(true)
|
|
72
|
+
try {
|
|
73
|
+
await saveConfiguration(config)
|
|
74
|
+
await webApi.syncConfig(config)
|
|
75
|
+
showToast('Kredensial Google Workspace berhasil disimpan!')
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to save config:', error)
|
|
78
|
+
showToast('Gagal menyimpan kredensial.')
|
|
79
|
+
} finally {
|
|
80
|
+
setIsSaving(false)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const showToast = (msg) => {
|
|
85
|
+
setToastMessage(msg)
|
|
86
|
+
setTimeout(() => setToastMessage(null), 3000)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isLoading) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="min-h-screen bg-base-300 text-base-content flex items-center justify-center">
|
|
92
|
+
<span className="loading loading-spinner text-primary w-12 h-12"></span>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="h-screen bg-base-300 text-base-content overflow-hidden relative font-['Poppins',sans-serif]">
|
|
99
|
+
{/* Background Ambience */}
|
|
100
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,oklch(var(--n))_0%,transparent_70%)] opacity-20 pointer-events-none" />
|
|
101
|
+
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-10 pointer-events-none" />
|
|
102
|
+
|
|
103
|
+
{/* Main Content Area */}
|
|
104
|
+
<div className="relative z-10 w-full h-full overflow-y-auto custom-scrollbar">
|
|
105
|
+
<div className="max-w-4xl mx-auto px-4 py-8 pb-32 space-y-8">
|
|
106
|
+
{/* Page Header */}
|
|
107
|
+
<div className="flex items-center justify-between">
|
|
108
|
+
<div className="flex items-center gap-4">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => navigate('/')}
|
|
112
|
+
className="btn btn-ghost btn-sm btn-circle shrink-0"
|
|
113
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
114
|
+
title="Kembali"
|
|
115
|
+
>
|
|
116
|
+
<svg
|
|
117
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
118
|
+
width="1.2em"
|
|
119
|
+
height="1.2em"
|
|
120
|
+
fill="none"
|
|
121
|
+
viewBox="0 0 24 24"
|
|
122
|
+
stroke="currentColor"
|
|
123
|
+
strokeWidth={2}
|
|
124
|
+
>
|
|
125
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
126
|
+
</svg>
|
|
127
|
+
</button>
|
|
128
|
+
<div>
|
|
129
|
+
<h1 className="text-2xl font-bold">Google Workspace</h1>
|
|
130
|
+
<p className="opacity-50 text-sm mt-1">
|
|
131
|
+
Integrasi Drive, Calendar, dan Gmail ke Mark AI.
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{/* Info Card */}
|
|
137
|
+
<div className="card bg-base-100 border border-info/30 shadow-xl shadow-info/10">
|
|
138
|
+
<div className="card-body">
|
|
139
|
+
<h2 className="card-title text-info mb-2 flex items-center gap-2">
|
|
140
|
+
<FaExclamationTriangle /> Panduan Singkat
|
|
141
|
+
</h2>
|
|
142
|
+
<p className="text-sm opacity-80 mb-4">
|
|
143
|
+
Untuk menghubungkan Mark dengan Google Workspace milikmu, kamu harus membuat{' '}
|
|
144
|
+
<strong>OAuth Client ID</strong> bertipe Desktop App di Google Cloud Console.
|
|
145
|
+
</p>
|
|
146
|
+
<ol className="list-decimal list-inside text-sm space-y-2 opacity-80 bg-base-200 p-4 rounded-xl font-mono">
|
|
147
|
+
<li>
|
|
148
|
+
Buka{' '}
|
|
149
|
+
<a
|
|
150
|
+
href="https://console.cloud.google.com/"
|
|
151
|
+
target="_blank"
|
|
152
|
+
rel="noreferrer"
|
|
153
|
+
className="text-primary hover:underline"
|
|
154
|
+
>
|
|
155
|
+
console.cloud.google.com
|
|
156
|
+
</a>
|
|
157
|
+
</li>
|
|
158
|
+
<li>
|
|
159
|
+
Buat Project Baru & Aktifkan: <strong>Drive API, Gmail API, Calendar API</strong>
|
|
160
|
+
</li>
|
|
161
|
+
<li>
|
|
162
|
+
Buat <strong>OAuth Consent Screen</strong> (External, masukkan emailmu di Test
|
|
163
|
+
Users)
|
|
164
|
+
</li>
|
|
165
|
+
<li>
|
|
166
|
+
Buat Credentials baru: <strong>OAuth client ID</strong> >{' '}
|
|
167
|
+
<strong>Desktop App</strong>
|
|
168
|
+
</li>
|
|
169
|
+
<li>Copy Client ID & Client Secret ke form di bawah ini.</li>
|
|
170
|
+
</ol>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Form Card */}
|
|
175
|
+
<div className="card bg-base-100 border border-white/5 shadow-2xl">
|
|
176
|
+
<div className="card-body space-y-6">
|
|
177
|
+
<h2 className="card-title text-xl border-b border-white/10 pb-4 flex items-center gap-2">
|
|
178
|
+
<FaKey className="text-warning" /> API Credentials
|
|
179
|
+
</h2>
|
|
180
|
+
|
|
181
|
+
<div className="form-control w-full">
|
|
182
|
+
<label className="label">
|
|
183
|
+
<span className="label-text font-bold text-sm">Google Client ID</span>
|
|
184
|
+
</label>
|
|
185
|
+
<input
|
|
186
|
+
type="text"
|
|
187
|
+
placeholder="Misal: 123456789-abcdefghijkl.apps.googleusercontent.com"
|
|
188
|
+
className="input input-bordered w-full font-mono text-sm focus:border-primary"
|
|
189
|
+
value={config.googleClientId || ''}
|
|
190
|
+
onChange={(e) => setConfig({ ...config, googleClientId: e.target.value })}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="form-control w-full">
|
|
195
|
+
<label className="label">
|
|
196
|
+
<span className="label-text font-bold text-sm">Google Client Secret</span>
|
|
197
|
+
</label>
|
|
198
|
+
<input
|
|
199
|
+
type="password"
|
|
200
|
+
placeholder="GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
201
|
+
className="input input-bordered w-full font-mono text-sm focus:border-primary"
|
|
202
|
+
value={config.googleClientSecret || ''}
|
|
203
|
+
onChange={(e) => setConfig({ ...config, googleClientSecret: e.target.value })}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="bg-warning/10 border border-warning/30 rounded-xl p-4 mt-4 flex gap-3 items-start">
|
|
208
|
+
<FaExclamationTriangle className="text-warning mt-1 shrink-0" />
|
|
209
|
+
<div>
|
|
210
|
+
<h3 className="font-bold text-warning text-sm">Peringatan Keamanan</h3>
|
|
211
|
+
<p className="text-xs opacity-70 mt-1">
|
|
212
|
+
Kredensial ini disimpan secara lokal dan aman di databasemu. Jangan pernah
|
|
213
|
+
membagikan Client Secret ke orang lain. Mark hanya akan menggunakan kredensial
|
|
214
|
+
ini untuk meminta akses ke akun Google-mu secara resmi.
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Connection Status & Buttons */}
|
|
220
|
+
<div className="border-t border-white/10 pt-6 mt-2 flex flex-col gap-4">
|
|
221
|
+
<div className="flex items-center justify-between">
|
|
222
|
+
<div>
|
|
223
|
+
<h3 className="font-bold text-sm">Status Koneksi</h3>
|
|
224
|
+
<p className="text-xs opacity-70">
|
|
225
|
+
{isConnected
|
|
226
|
+
? 'Mark terhubung dan memiliki akses ke Google Workspace.'
|
|
227
|
+
: 'Mark belum diberikan akses masuk ke Google Workspace.'}
|
|
228
|
+
</p>
|
|
229
|
+
</div>
|
|
230
|
+
<div
|
|
231
|
+
className={`px-3 py-1 rounded-full text-xs font-bold ${isConnected ? 'bg-success/20 text-success' : 'bg-base-300 text-white/50'}`}
|
|
232
|
+
>
|
|
233
|
+
{isConnected ? 'TERHUBUNG' : 'TERPUTUS'}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{isConnected ? (
|
|
238
|
+
<button onClick={handleDisconnect} className="btn btn-error btn-outline w-full">
|
|
239
|
+
Putuskan Koneksi Google
|
|
240
|
+
</button>
|
|
241
|
+
) : (
|
|
242
|
+
<button
|
|
243
|
+
onClick={handleConnect}
|
|
244
|
+
disabled={isConnecting || !config.googleClientId || !config.googleClientSecret}
|
|
245
|
+
className="btn btn-info w-full"
|
|
246
|
+
>
|
|
247
|
+
{isConnecting ? (
|
|
248
|
+
<span className="loading loading-spinner"></span>
|
|
249
|
+
) : (
|
|
250
|
+
<FaGoogle />
|
|
251
|
+
)}
|
|
252
|
+
Login & Beri Akses
|
|
253
|
+
</button>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Sticky Save Button */}
|
|
261
|
+
<div className="fixed bottom-0 left-0 w-full bg-base-300/80 backdrop-blur border-t border-white/10 p-4 z-50">
|
|
262
|
+
<div className="max-w-5xl mx-auto flex justify-end">
|
|
263
|
+
<button
|
|
264
|
+
onClick={handleSave}
|
|
265
|
+
disabled={isSaving || !config.googleClientId || !config.googleClientSecret}
|
|
266
|
+
className="btn btn-primary min-w-[200px]"
|
|
267
|
+
>
|
|
268
|
+
{isSaving ? (
|
|
269
|
+
<span className="loading loading-spinner"></span>
|
|
270
|
+
) : (
|
|
271
|
+
<>
|
|
272
|
+
<FaSave /> Simpan Kredensial
|
|
273
|
+
</>
|
|
274
|
+
)}
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{/* Toast Notification */}
|
|
280
|
+
{toastMessage && (
|
|
281
|
+
<div className="toast toast-top toast-center z-[100]">
|
|
282
|
+
<div className="alert alert-success shadow-lg">
|
|
283
|
+
<FaCheckCircle className="text-xl" />
|
|
284
|
+
<span>{toastMessage}</span>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default GoogleWorkspace
|