Skip to content

Commit fa356b5

Browse files
committed
feat: Integrate Coach Artie local AI assistant
- Added useCoachArtie composable for local AI interaction - Enhanced App.vue to support Coach Artie model and mode - Updated components to display Coach Artie-specific UI elements - Implemented Coach Artie connection and message handling - Added diagnostic and refresh methods for Obsidian vault integration - Refined status bar and title bar to show Coach Artie availability - Improved error handling and logging for local AI interactions
1 parent a818dca commit fa356b5

18 files changed

+1551
-480
lines changed

src/App.vue

+118-9
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
z-40: Modal backdrops
88
z-50: Modals/dialogs/overlays
99
-->
10-
<div class="h-screen flex flex-col bg-white dark:bg-oled-black">
10+
<div id="app" class="app-container h-screen flex flex-col" :class="{ 'coach-artie-mode': isCoachArtieMode }">
1111
<!-- Base App Shell -->
1212
<div v-if="isAuthenticated" class="relative flex flex-col flex-1 min-h-0">
1313
<!-- Title Bar -->
@@ -71,8 +71,9 @@
7171
<ChatInput v-model="messageInput" :is-loading="isSending" :has-valid-key="hasValidKey"
7272
:show-mention-popup="showMentionPopup" :is-searching-files="isSearchingFiles"
7373
:has-obsidian-vault="hasObsidianVault" :obsidian-search-results="obsidianSearchResults"
74-
@send="handleSendMessage" @mention-popup="(show) => showMentionPopup = show"
75-
@obsidian-link="handleObsidianLink" @input="(query) => searchQuery = query" />
74+
:obsidian-vault-path="obsidian.vaultPath.value" @send="handleSendMessage"
75+
@mention-popup="(show) => showMentionPopup = show" @obsidian-link="handleObsidianLink"
76+
@input="(query) => searchQuery = query" />
7677
</div>
7778
</main>
7879
</div>
@@ -144,6 +145,7 @@ import { useSupabase } from './composables/useSupabase'
144145
import { useAIChat } from './composables/useAIChat'
145146
import { useStore } from './lib/store'
146147
import { useObsidianFiles } from './composables/useObsidianFiles'
148+
import { useCoachArtie } from './composables/useCoachArtie'
147149
import type { ObsidianFile } from './types'
148150
149151
// Component imports
@@ -180,6 +182,7 @@ const { loadChatHistories, saveChatHistory, deleteAllChats } = useSupabase()
180182
const { isDark, systemPrefersDark } = useTheme()
181183
const aiChat = useAIChat()
182184
const openRouter = useOpenRouter()
185+
const coachArtie = useCoachArtie()
183186
const store = useStore()
184187
const obsidian = useObsidianFiles()
185188
@@ -211,6 +214,7 @@ const chatContainerRef = ref<HTMLElement | null>(null)
211214
const messageInput = ref('')
212215
const isKeyboardShortcutsModalOpen = ref(false)
213216
const isClearChatHistoryModalOpen = ref(false)
217+
const isDevelopmentMode = ref(import.meta.env.DEV)
214218
215219
// =========================================
216220
// Chat state
@@ -242,17 +246,44 @@ const preferences = useLocalStorage('preferences', {
242246
// Computed properties
243247
// =========================================
244248
const availableModels = computed(() => {
245-
return openRouter.availableModels.value?.map(model => ({
249+
// Create Coach Artie model
250+
const coachArtieModel = {
251+
id: 'coach-artie',
252+
name: '🤖 Coach Artie',
253+
description: 'Memory-enhanced local AI assistant',
254+
context_length: 100000, // Arbitrary large context size
255+
pricing: {
256+
prompt: '0',
257+
completion: '0'
258+
},
259+
capabilities: {
260+
vision: false,
261+
tools: true,
262+
function_calling: true
263+
},
264+
provider: 'local'
265+
};
266+
267+
// Get OpenRouter models
268+
const openRouterModels = openRouter.availableModels.value?.map(model => ({
246269
...model,
247270
name: model.name || model.id.split('/').pop() || '',
248271
description: model.description
249-
})) || []
272+
})) || [];
273+
274+
// Add Coach Artie at the top if it's available
275+
return coachArtie.isConnected.value
276+
? [coachArtieModel, ...openRouterModels]
277+
: openRouterModels;
250278
})
251279
252280
const shouldShowWelcome = computed(() => {
253281
return messages.value.length === 0 && chatHistory.value.length === 0
254282
})
255283
284+
// Add a computed property to check if Coach Artie is the current model
285+
const isCoachArtieMode = computed(() => currentModel.value === 'coach-artie')
286+
256287
// =========================================
257288
// Theme handling
258289
// =========================================
@@ -718,6 +749,31 @@ onMounted(async () => {
718749
logger.debug('Cleaning up IPC listeners')
719750
cleanupFns.forEach(cleanup => cleanup && cleanup())
720751
})
752+
753+
// Sync the hasObsidianVault with the composable's computed property
754+
hasObsidianVault.value = obsidian.hasVault.value;
755+
console.log('Initial Obsidian state:');
756+
console.log(' - hasVault from composable:', obsidian.hasVault.value);
757+
console.log(' - hasObsidianVault in App.vue:', hasObsidianVault.value);
758+
759+
// Ensure Obsidian vault is loaded with a slight delay to ensure store is ready
760+
setTimeout(async () => {
761+
await refreshObsidianVault();
762+
763+
// Try a test search to verify functionality if we have a vault
764+
if (obsidian.hasVault.value) {
765+
try {
766+
isSearchingFiles.value = true;
767+
await obsidian.searchFiles('test');
768+
console.log('Test search after init:', obsidian.searchResults.value?.length || 0, 'results');
769+
obsidianSearchResults.value = obsidian.searchResults.value;
770+
} catch (err) {
771+
console.error('Test search failed:', err);
772+
} finally {
773+
isSearchingFiles.value = false;
774+
}
775+
}
776+
}, 1500);
721777
})
722778
723779
// Watch messages and scroll to bottom when they change
@@ -818,30 +874,74 @@ const renameChat = async (id: string, newTitle: string) => {
818874
}
819875
}
820876
821-
// Initialize obsidian integration
877+
// Watch for changes to the Obsidian vault status
822878
watch(() => obsidian.hasVault.value, (hasVault) => {
823-
hasObsidianVault.value = hasVault
824-
logger.debug('Obsidian vault detected:', hasVault)
825-
})
879+
console.log('Obsidian vault status changed:', hasVault);
880+
console.log(' - Path:', obsidian.vaultPath.value);
881+
console.log(' - Path exists:', obsidian.pathExists.value);
882+
883+
// Synchronize the UI state with the composable state
884+
hasObsidianVault.value = hasVault;
885+
}, { immediate: true })
826886
827887
// Watch for search query changes
828888
watch(() => searchQuery.value, async (query) => {
829889
console.log('App.vue - searchQuery changed:', query)
830890
console.log('App.vue - hasObsidianVault:', obsidian.hasVault.value)
891+
console.log('App.vue - Obsidian vault path:', obsidian.vaultPath.value)
831892
832893
if (query && obsidian.hasVault.value) {
833894
isSearchingFiles.value = true
834895
console.log('App.vue - Before searchFiles call')
896+
897+
// Add extra debug logging to trace the search process
898+
console.log('DEBUG - About to search files with query:', query)
899+
console.log('DEBUG - Current vault path:', obsidian.vaultPath.value)
900+
console.log('DEBUG - Is path valid:', !!obsidian.vaultPath.value && obsidian.vaultPath.value.length > 0)
901+
902+
// Perform the search
835903
await obsidian.searchFiles(query)
904+
905+
// Log results for debugging
836906
console.log('App.vue - After searchFiles call')
837907
console.log('App.vue - searchResults:', obsidian.searchResults.value)
908+
console.log('DEBUG - Search results count:', obsidian.searchResults.value?.length || 0)
909+
838910
obsidianSearchResults.value = obsidian.searchResults.value as ObsidianFile[]
839911
isSearchingFiles.value = false
840912
logger.debug('Obsidian search results:', obsidianSearchResults.value.length)
841913
} else {
842914
obsidianSearchResults.value = []
843915
}
844916
})
917+
918+
// Watch for Coach Artie availability and select it when it becomes available
919+
watch(() => coachArtie.isConnected.value, (isConnected) => {
920+
if (isConnected) {
921+
logger.debug('Coach Artie is now available, selecting it as the current model');
922+
aiChat.setModel('coach-artie');
923+
}
924+
})
925+
926+
// Add this method to the script section
927+
const refreshObsidianVault = async () => {
928+
console.log('🔄 Refreshing Obsidian vault path');
929+
try {
930+
// First, reload the vault path from store
931+
await obsidian.loadVaultPath();
932+
933+
// Update the UI flag - use the computed value instead of path existence
934+
hasObsidianVault.value = obsidian.hasVault.value;
935+
936+
console.log('🔄 Vault refresh complete:');
937+
console.log(' - Path:', obsidian.vaultPath.value);
938+
console.log(' - Path exists:', obsidian.pathExists.value);
939+
console.log(' - Has vault:', obsidian.hasVault.value);
940+
console.log(' - UI state:', hasObsidianVault.value);
941+
} catch (err) {
942+
console.error('🔄 Failed to refresh vault path:', err);
943+
}
944+
};
845945
</script>
846946

847947
<style scoped>
@@ -1033,4 +1133,13 @@ watch(() => searchQuery.value, async (query) => {
10331133
max-width: none;
10341134
}
10351135
}
1136+
1137+
.coach-artie-mode {
1138+
--coach-artie-accent: rgba(79, 70, 229, 0.1);
1139+
background-image: linear-gradient(to bottom right, var(--coach-artie-accent), transparent);
1140+
}
1141+
1142+
.dark .coach-artie-mode {
1143+
--coach-artie-accent: rgba(79, 70, 229, 0.05);
1144+
}
10361145
</style>

src/components/ChatInput.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const props = defineProps<{
1111
isSearchingFiles: boolean
1212
hasObsidianVault: boolean
1313
obsidianSearchResults: ObsidianFile[]
14+
obsidianVaultPath: string
1415
}>()
1516
1617
const emit = defineEmits<{
@@ -392,7 +393,8 @@ const removeFile = (fileToRemove: IncludedFile) => {
392393

393394
<!-- File mention popup -->
394395
<ObsidianMentionPopup :show="showMentionPopup" :results="obsidianSearchResults" :is-searching="isSearchingFiles"
395-
:has-vault="hasObsidianVault" @select="insertObsidianLink" @close="() => emit('mention-popup', false)" />
396+
:hasVault="hasObsidianVault" :path="obsidianVaultPath" @select="insertObsidianLink"
397+
@close="() => emit('mention-popup', false)" />
396398

397399
<!-- Included files preview -->
398400
<div v-if="messageIncludedFiles.length > 0"

src/components/ChatMessage.vue

+33-3
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,35 @@ const messageModelName = computed(() => {
2828
if (props.message.role !== 'assistant' || !props.message.model) {
2929
return props.modelName
3030
}
31+
32+
// Check if this is a Coach Artie message
33+
if (props.message.model === 'coach-artie') {
34+
return '🤖 Coach Artie'
35+
}
36+
3137
// Extract the model name from the full model ID (e.g., "anthropic/claude-3-sonnet" -> "claude-3-sonnet")
3238
return props.message.model.split('/').pop() || props.modelName
3339
})
40+
41+
// Check if this message is from Coach Artie
42+
const isCoachArtieMessage = computed(() => {
43+
return props.message.model === 'coach-artie'
44+
})
3445
</script>
3546

3647
<template>
3748
<div class="group relative flex gap-3 py-3" :class="{
38-
'opacity-50': message.isStreaming
49+
'opacity-50': message.isStreaming,
50+
'coach-artie-message': isCoachArtieMessage && message.role === 'assistant'
3951
}">
4052
<!-- Role Icon -->
4153
<div class="flex h-8 w-8 flex-none items-center justify-center rounded-lg" :class="{
42-
'bg-blue-500/10 text-blue-400': message.role === 'assistant',
54+
'bg-blue-500/10 text-blue-400': message.role === 'assistant' && !isCoachArtieMessage,
55+
'bg-indigo-500/10 text-indigo-400': message.role === 'assistant' && isCoachArtieMessage,
4356
'bg-gray-500/10 text-gray-400': message.role === 'user'
4457
}">
45-
<Icon v-if="message.role === 'assistant'" icon="carbon:bot" class="h-5 w-5" />
58+
<Icon v-if="message.role === 'assistant' && isCoachArtieMessage" icon="mdi:robot" class="h-5 w-5" />
59+
<Icon v-else-if="message.role === 'assistant'" icon="carbon:bot" class="h-5 w-5" />
4660
<Icon v-else icon="carbon:user" class="h-5 w-5" />
4761
</div>
4862

@@ -102,6 +116,22 @@ const messageModelName = computed(() => {
102116
</template>
103117

104118
<style scoped>
119+
/* Add styles for Coach Artie messages */
120+
.coach-artie-message {
121+
position: relative;
122+
}
123+
124+
.coach-artie-message::before {
125+
content: '';
126+
position: absolute;
127+
left: -0.5rem;
128+
top: 0;
129+
bottom: 0;
130+
width: 2px;
131+
background: linear-gradient(to bottom, rgba(79, 70, 229, 0.5), rgba(79, 70, 229, 0.1));
132+
border-radius: 1px;
133+
}
134+
105135
:deep(.prose) {
106136
font-size: 0.875rem;
107137
line-height: 1.5;

0 commit comments

Comments
 (0)