Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

[![Сайт и демо админки](https://img.shields.io/badge/Сайт_и_демо_админки-ai--sekretar24.ru-4285F4?style=for-the-badge&logo=google-chrome&logoColor=white)](https://ai-sekretar24.ru/)
[![Telegram поддержка](https://img.shields.io/badge/Поддержка_и_ассистент-@ai__sekretar24bot-26A5E4?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/ai_sekretar24bot)
[![Android APK](https://img.shields.io/badge/Android_APK-v2.2-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://github.com/ShaerWare/AI_Secretary_System/releases/download/mobile-v2.2/ai-secretary-2.2.apk)
[![Android APK](https://img.shields.io/badge/Android_APK-v2.3-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://github.com/ShaerWare/AI_Secretary_System/releases/download/mobile-v2.3/ai-secretary-2.3.apk)

[Сайт проекта и демо админки](https://ai-sekretar24.ru/) (логин/пароль: `admin` / `admin`) | [Telegram поддержки и ассистент проекта](https://t.me/ai_sekretar24bot) | [Wiki](https://github.com/ShaerWare/AI_Secretary_System/wiki) | [Issues](https://github.com/ShaerWare/AI_Secretary_System/issues) | [Android APK](https://github.com/ShaerWare/AI_Secretary_System/releases/latest)

Expand Down Expand Up @@ -563,13 +563,13 @@ curl -X POST http://localhost:8002/admin/telegram/instances/{id}/start

**Скачать APK:**

📱 [**ai-secretary-2.2.apk**](https://github.com/ShaerWare/AI_Secretary_System/releases/download/mobile-v2.2/ai-secretary-2.2.apk) — последняя сборка (debug, не подписана для Play Store)
📱 [**ai-secretary-2.3.apk**](https://github.com/ShaerWare/AI_Secretary_System/releases/download/mobile-v2.3/ai-secretary-2.3.apk) — последняя сборка (debug, не подписана для Play Store)

Все релизы: [github.com/ShaerWare/AI_Secretary_System/releases](https://github.com/ShaerWare/AI_Secretary_System/releases)

**Установка через adb:**
```bash
adb install -r ai-secretary-2.2.apk
adb install -r ai-secretary-2.3.apk
```

Или скопируйте APK на телефон и откройте в файловом менеджере (разрешите «Установка из неизвестных источников»). При ошибке `INSTALL_FAILED_UPDATE_INCOMPATIBLE` сначала удалите старую версию: `adb uninstall com.shaerware.aisecretary`.
Expand Down
10 changes: 9 additions & 1 deletion admin/src/api/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { api, createSSE, getAuthHeaders } from './client'

Check warning on line 1 in admin/src/api/chat.ts

View workflow job for this annotation

GitHub Actions / lint-frontend

'createSSE' is defined but never used. Allowed unused vars must match /^_/u

export interface ChatImage {
id: string
Expand Down Expand Up @@ -152,12 +152,20 @@
getSession: (sessionId: string) =>
api.get<{ session: ChatSession }>(`/admin/chat/sessions/${sessionId}`),

createSession: (title?: string, systemPrompt?: string, source?: string, sourceId?: string) =>
createSession: (
title?: string,
systemPrompt?: string,
source?: string,
sourceId?: string,
options?: { knowledgeCollectionIds?: number[]; ragMode?: string | null },
) =>
api.post<{ session: ChatSession }>('/admin/chat/sessions', {
title,
system_prompt: systemPrompt,
source,
source_id: sourceId,
knowledge_collection_ids: options?.knowledgeCollectionIds,
rag_mode: options?.ragMode,
}),

updateSession: (sessionId: string, data: { title?: string; system_prompt?: string; pinned?: boolean; context_files?: { name: string; content: string }[]; rag_mode?: string; knowledge_collection_ids?: number[]; web_search_enabled?: boolean }) =>
Expand Down Expand Up @@ -271,7 +279,7 @@
let buffer = ''
let receivedDone = false

while (true) {

Check warning on line 282 in admin/src/api/chat.ts

View workflow job for this annotation

GitHub Actions / lint-frontend

Unexpected constant condition
const { done, value } = await reader.read()
if (done) break

Expand Down
1 change: 1 addition & 0 deletions admin/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export * from './workspace'
export * from './githubRepos'
export * from './google'
export * from './rssFeeds'
export * from './presets'
23 changes: 23 additions & 0 deletions admin/src/api/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { api } from './client'

export interface PresetCollection {
id: number
slug: string
name: string
}

export interface AssistantPreset {
slug: string
name: string
description: string
icon: string
system_prompt: string | null
rag_mode: string | null
collections: PresetCollection[]
knowledge_collection_ids: number[]
ready: boolean
}

export const presetsApi = {
list: () => api.get<{ presets: AssistantPreset[] }>('/admin/chat/assistant-presets'),
}
124 changes: 124 additions & 0 deletions admin/src/components/AssistantPresetPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { presetsApi, type AssistantPreset } from '@/api/presets'
import { Scale, Calculator, Search, Bot, Edit3, X } from 'lucide-vue-next'

const props = defineProps<{ open: boolean }>()
const emit = defineEmits<{
(e: 'select', preset: AssistantPreset): void
(e: 'close'): void
}>()

const presets = ref<AssistantPreset[]>([])
const loading = ref(false)
const error = ref<string | null>(null)

async function loadPresets() {
loading.value = true
error.value = null
try {
const resp = await presetsApi.list()
presets.value = resp.presets
} catch (e) {
error.value = e instanceof Error ? e.message : 'Не удалось загрузить'
} finally {
loading.value = false
}
}

watch(
() => props.open,
(now) => {
if (now && !presets.value.length) loadPresets()
},
)

onMounted(() => {
if (props.open) loadPresets()
})

const visiblePresets = computed(() => presets.value.filter((p) => p.ready))

const iconMap = {
scale: Scale,
calculator: Calculator,
search: Search,
bot: Bot,
edit: Edit3,
} as const

function getIcon(name: string) {
return iconMap[name as keyof typeof iconMap] || Bot
}
</script>

<template>
<div
v-if="open"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
@click.self="emit('close')"
>
<div
class="w-full max-w-2xl max-h-[80vh] flex flex-col bg-stone-900 border border-stone-700 rounded-2xl shadow-2xl overflow-hidden"
>
<!-- Header -->
<div class="shrink-0 flex items-center justify-between px-5 py-4 border-b border-stone-800">
<div>
<h2 class="text-lg font-semibold text-white">Новый ассистент</h2>
<p class="text-xs text-stone-400 mt-0.5">
Выберите тематику — к ассистенту автоматически прикрепятся коллекции на эту тему
</p>
</div>
<button
class="text-stone-500 hover:text-white transition-colors p-1"
@click="emit('close')"
>
<X class="w-5 h-5" />
</button>
</div>

<!-- Body -->
<div class="flex-1 overflow-y-auto p-4">
<div v-if="loading" class="flex items-center justify-center py-12">
<div
class="w-6 h-6 border-2 border-amber-500 border-t-transparent rounded-full animate-spin"
/>
</div>
<div v-else-if="error" class="flex flex-col items-center justify-center py-12 text-center">
<p class="text-red-400 text-sm mb-3">{{ error }}</p>
<button
class="px-4 py-2 rounded-lg bg-amber-600 hover:bg-amber-700 text-white text-sm transition-colors"
@click="loadPresets"
>
Повторить
</button>
</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
v-for="p in visiblePresets"
:key="p.slug"
class="flex items-start gap-3 p-4 rounded-xl bg-stone-800/60 hover:bg-stone-700/80 active:bg-stone-700 border border-stone-700/50 hover:border-amber-600/40 transition-all text-left group"
@click="emit('select', p)"
>
<div
class="shrink-0 w-11 h-11 rounded-lg bg-amber-600/15 text-amber-400 flex items-center justify-center group-hover:bg-amber-600/25 transition-colors"
>
<component :is="getIcon(p.icon)" class="w-5 h-5" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-white">{{ p.name }}</div>
<div class="text-xs text-stone-400 mt-1 line-clamp-2">{{ p.description }}</div>
<div v-if="p.collections.length > 0" class="text-[11px] text-amber-500/80 mt-2">
{{ p.collections.length }}
{{ p.collections.length === 1 ? 'коллекция' : 'коллекций' }}
</div>
<div v-else-if="p.slug === 'custom'" class="text-[11px] text-stone-500 mt-2">
Без коллекций — настроить позже
</div>
</div>
</button>
</div>
</div>
</div>
</div>
</template>
32 changes: 30 additions & 2 deletions admin/src/views/ChatView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ import {
Palette
} from 'lucide-vue-next'
import UserProfileModal from '@/components/UserProfileModal.vue'
import AssistantPresetPicker from '@/components/AssistantPresetPicker.vue'
import type { AssistantPreset } from '@/api/presets'
import { useSidebarCollapse } from '@/composables/useSidebarCollapse'
import { useClaudeCode } from '@/composables/useClaudeCode'
import { claudeCodeApi, type CcProject, type CcProjectInput } from '@/api/claudeCode'
Expand Down Expand Up @@ -1061,13 +1063,32 @@ watch(selectedPromptId, (id) => {

// Mutations
const createSessionMutation = useMutation({
mutationFn: () => chatApi.createSession(undefined, undefined, 'admin'),
mutationFn: (preset?: AssistantPreset) =>
chatApi.createSession(
undefined,
preset?.system_prompt ?? undefined,
'admin',
undefined,
preset
? {
knowledgeCollectionIds: preset.knowledge_collection_ids,
ragMode: preset.rag_mode ?? undefined,
}
: undefined,
),
onSuccess: (data) => {
refetchSessions()
currentSessionId.value = data.session.id
},
})

const showPresetPicker = ref(false)

function handlePresetSelect(preset: AssistantPreset) {
showPresetPicker.value = false
createSessionMutation.mutate(preset)
}

const deleteSessionMutation = useMutation({
mutationFn: (sessionId: string) => chatApi.deleteSession(sessionId),
onSuccess: () => {
Expand Down Expand Up @@ -1299,7 +1320,7 @@ watch(currentSessionId, () => {
})

function createNewChat() {
createSessionMutation.mutate()
showPresetPicker.value = true
}

async function deleteCurrentSession() {
Expand Down Expand Up @@ -4696,6 +4717,13 @@ class="w-4 h-4 rounded border flex items-center justify-center shrink-0"

<!-- User Profile modal (chat-only users open it from the focus-mode toolbar) -->
<UserProfileModal v-model="showUserProfile" />

<!-- Assistant preset picker — opens on "+" new-chat click -->
<AssistantPresetPicker
:open="showPresetPicker"
@select="handlePresetSelect"
@close="showPresetPicker = false"
/>
</template>

<style scoped>
Expand Down
4 changes: 2 additions & 2 deletions admin/src/views/LoginView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const showAbout = ref(false)
const activeTab = ref<'ai' | 'privacy' | 'features' | 'channels' | 'cases'>('cases')

// Mobile app version — fetched from /admin/mobile/version (driven by MOBILE_LATEST_* env on server)
const mobileVersion = ref('2.2')
const mobileVersion = ref('2.3')
const mobileApkUrl = ref(
'https://github.com/ShaerWare/AI_Secretary_System/releases/latest/download/ai-secretary-2.2.apk',
'https://github.com/ShaerWare/AI_Secretary_System/releases/latest/download/ai-secretary-2.3.apk',
)

async function fetchMobileVersion() {
Expand Down
4 changes: 2 additions & 2 deletions mobile/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ android {
applicationId "com.shaerware.aisecretary"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 25
versionName "2.2"
versionCode 26
versionName "2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
23 changes: 19 additions & 4 deletions mobile/src/api/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,31 @@ export const chatApi = {
`/admin/chat/sessions/${id}`,
),

createSession: (title?: string, options?: { skipInstancePrompt?: boolean }) => {
createSession: (
title?: string,
options?: {
skipInstancePrompt?: boolean;
systemPrompt?: string | null;
knowledgeCollectionIds?: number[];
ragMode?: string | null;
},
) => {
const config = useMobileConfigStore();
const instanceId = config.instance?.id;
// Explicit systemPrompt (from preset picker) wins over instance default.
const systemPrompt =
options?.systemPrompt !== undefined
? options.systemPrompt
: options?.skipInstancePrompt
? undefined
: config.instance?.system_prompt || undefined;
return api.post<{ session: ChatSession }>("/admin/chat/sessions", {
title,
source: "mobile",
source_id: instanceId || undefined,
system_prompt: options?.skipInstancePrompt
? undefined
: config.instance?.system_prompt || undefined,
system_prompt: systemPrompt,
knowledge_collection_ids: options?.knowledgeCollectionIds,
rag_mode: options?.ragMode,
});
},

Expand Down
24 changes: 24 additions & 0 deletions mobile/src/api/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { api } from "./client";

export interface PresetCollection {
id: number;
slug: string;
name: string;
}

export interface AssistantPreset {
slug: string;
name: string;
description: string;
icon: string;
system_prompt: string | null;
rag_mode: string | null;
collections: PresetCollection[];
knowledge_collection_ids: number[];
ready: boolean;
}

export const presetsApi = {
list: () =>
api.get<{ presets: AssistantPreset[] }>("/admin/chat/assistant-presets"),
};
Loading
Loading