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
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ android {
applicationId "app.aiaw.glass"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20018
versionName "2.0.8.9"
versionCode 20019
versionName "2.0.8.12"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
4 changes: 2 additions & 2 deletions android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}

Expand Down
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/java/app/aiaw/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package app.aiaw;

import android.os.Bundle;
import android.view.WindowManager;

import com.getcapacitor.BridgeActivity;

public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(LocalFsPlugin.class);
// Set this before Capacitor creates the WebView; its edge-to-edge
// initialization otherwise overwrites the manifest/runtime mode.
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
super.onCreate(savedInstanceState);
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aiaw",
"version": "2.0.8.9",
"version": "2.0.8.12",
"description": "精心设计的 AI (LLM) 客户端。全功能,轻量级;支持多工作区、插件系统、跨平台、本地优先+实时云同步",
"productName": "AI as Workspace",
"author": "NitroRCr <i@krytro.com>",
Expand Down
1 change: 1 addition & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ onMounted(() => {

async function migrateBuiltinPluginsAtStartup() {
try {
if (!db.isOpen()) await db.open()
const assistants = await db.assistants.toArray()
for (const assistant of assistants) {
const beforeKeys = Object.keys(assistant.plugins || {})
Expand Down
4 changes: 2 additions & 2 deletions src/components/MessageInfoDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ const props = defineProps<{
message: Message
}>()

const length = computed(() => props.message.contents.filter(
c => c.type === 'assistant-message' || c.type === 'user-message'
const length = computed(() => (props.message.contents || []).filter(
c => (c.type === 'assistant-message' || c.type === 'user-message') && typeof c.text === 'string'
).reduce((prev, cur) => prev + cur.text.length, 0))
const createdAt = computed(() => idDateString(props.message.id))

Expand Down
39 changes: 27 additions & 12 deletions src/components/MessageItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ const props = defineProps<{
message: Message,
childNum: number,
scrollContainer: HTMLElement,
lazyPlainText?: boolean,
branchControl?: {
current: number,
max: number,
Expand All @@ -416,7 +417,7 @@ function moreInfo() {
}
const sourceCodeMode = ref(false)

const contents = computed(() => props.message.contents.map(x => {
const contents = computed(() => (props.message.contents || []).map(x => {
if (x.type === 'assistant-message' || x.type === 'user-message') {
return {
...x,
Expand Down Expand Up @@ -445,7 +446,7 @@ watchEffect(async () => {
generatingSession: null,
status: 'failed',
error: 'aborted',
contents: props.message.contents.map(content => {
contents: (props.message.contents || []).map(content => {
if (content.type === 'assistant-tool' && content.status === 'calling') {
return {
...content,
Expand All @@ -459,8 +460,11 @@ watchEffect(async () => {
}
})

const textIndex = computed(() => props.message.contents.findIndex(c => ['user-message', 'assistant-message'].includes(c.type)))
const textContent = computed(() => (props.message.contents[textIndex.value] as UserMessageContent | AssistantMessageContent))
const textIndex = computed(() => (props.message.contents || []).findIndex(c => ['user-message', 'assistant-message'].includes(c.type)))
const textContent = computed(() => {
const content = (props.message.contents || [])[textIndex.value]
return (content || { type: 'assistant-message', text: '' }) as UserMessageContent | AssistantMessageContent
})

const { perfs } = useUserPerfsStore()
const assistantsStore = useAssistantsStore()
Expand Down Expand Up @@ -517,6 +521,10 @@ function isStreamingAssistantText(content: MessageContent) {

function getStreamingRenderState(content: MessageContent) {
if (isStreamingAssistantText(content)) return streamingRenderState.value
// Off-screen messages: skip expensive KaTeX/markdown rendering
if (props.lazyPlainText && (content.type === 'assistant-message' || content.type === 'user-message')) {
return { mode: 'plain-text' as const, text: content.text }
}
if (content.type === 'assistant-message' || content.type === 'user-message') {
return { mode: 'final' as const, text: content.text }
}
Expand Down Expand Up @@ -602,7 +610,9 @@ function onSelect(mode: 'mouse' | 'touch') {
}
const range = selection.getRangeAt(0)
const targetRects = range.getBoundingClientRect()
const baseRects = textDiv.value[0].getBoundingClientRect()
const textElement = textDiv.value?.[0] as HTMLElement | undefined
if (!textElement) return
const baseRects = textElement.getBoundingClientRect()
floatBtnStyle.top = targetRects.top < 48 || mode === 'touch'
? targetRects.bottom - baseRects.top + 12 + 'px'
: targetRects.top - baseRects.top - 48 + 'px'
Expand Down Expand Up @@ -776,7 +786,7 @@ function shouldPromoteInlineMath(inlineMath: HTMLElement) {
}

function injectDisplayMathScroll() {
const el: HTMLElement = textDiv.value[0]
const el = textDiv.value?.[0] as HTMLElement | undefined
if (!el) return

clearDisplayMathScroll(el)
Expand All @@ -802,25 +812,30 @@ function injectDisplayMathScroll() {
}
function injectConvertArtifact() {
if (!isPlatformEnabled(perfs.artifactsEnabled)) return
const el: HTMLElement = textDiv.value[0]
const el = textDiv.value?.[0] as HTMLElement | undefined
if (!el) return
el.querySelectorAll('.md-editor-code').forEach(code => {
if (code.querySelector('.md-editor-convert-artifact')) return
const anchor = code.querySelector('.md-editor-collapse-tips')
const action = code.querySelector('.md-editor-code-action')
const source = code.querySelector('pre code')
if (!anchor || !action || !source) return
const btn = document.createElement('span')
btn.innerHTML = 'convert_to_text'
btn.classList.add('md-editor-convert-artifact')
btn.addEventListener('click', (ev) => {
ev.preventDefault()
ev.stopPropagation()
const text = code.querySelector('pre code').textContent
const lang = code.querySelector('pre code').getAttribute('language')
const text = source.textContent || ''
const lang = source.getAttribute('language') || ''
const pattern = new RegExp(`\`{3,}.*\\n${escapeRegex(text)}\\s*\`{3,}`, 'g')
convertArtifact(text, pattern, lang)
})
btn.title = t('messageItem.convertToArtifactBtn')
code.querySelector('.md-editor-code-action').insertBefore(btn, anchor)
code.querySelector<HTMLElement>('.md-editor-copy-button').title = t('messageItem.copyCode')
code.querySelector<HTMLElement>('.md-editor-collapse-tips').title = t('messageItem.fold')
action.insertBefore(btn, anchor)
const copyButton = code.querySelector<HTMLElement>('.md-editor-copy-button')
if (copyButton) copyButton.title = t('messageItem.copyCode')
anchor.title = t('messageItem.fold')
})
}
const mdPreviewProps = useMdPreviewProps()
Expand Down
2 changes: 1 addition & 1 deletion src/components/ModelItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ const avatar = computed<Avatar>(() => {
if (m.startsWith('grok') || m.startsWith('xai')) return { type: 'svg', name: 'grok' }
if (m.startsWith('kimi') || m.startsWith('moonshot')) return { type: 'svg', name: 'kimi-c' }
if (m.startsWith('doubao')) return { type: 'svg', name: 'doubao-c' }
return defaultAvatar(m[0].toUpperCase())
return defaultAvatar(m?.[0]?.toUpperCase() || '?')
})
</script>
3 changes: 2 additions & 1 deletion src/components/ParseFilesDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ async function parse() {
if (!value) return []
const file = props.files[index]
const fp = fileparsers.value.find(fp => fp.id === value)
if (!fp || !file) return []
try {
const result = await fp.execute({ file, range: ranges[index] }, fp.settings)
return result.map(r => ({ ...r, name: file.name }))
Expand All @@ -178,7 +179,7 @@ async function parse() {
}

const ranges = reactive(props.files.map(() => null))
const selected = reactive(props.files.map((val, index) => allOptions.value[index][0]))
const selected = reactive(props.files.map((val, index) => allOptions.value[index]?.[0]).filter(Boolean))

const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
</script>
84 changes: 51 additions & 33 deletions src/components/SearchDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,56 +102,71 @@ interface Doc {
content: string
}

const global = ref(false)
const dialogs = ref<Dialog[]>(null)
const docs = ref<Doc[]>(null)
watchEffect(async () => {
dialogs.value = global.value
? await db.dialogs.toArray()
: await db.dialogs.where('workspaceId').equals(props.workspaceId).toArray()
const messages = global.value
? await db.messages.toArray()
: await db.messages.where('dialogId').anyOf(dialogs.value.map(d => d.id)).toArray()
docs.value = messages.map(m => ({
id: m.id,
dialogId: m.dialogId,
content: m.contents
.filter(c => c.type === 'assistant-message' || c.type === 'user-message')
.map(c => c.text)
.join('\n')
}))
search()
})

const q = ref('')
interface Result {
workspaceId: string
dialogId: string
title: string
route: number[]
preview?: string
}
const results = ref<Result[]>(null)

const global = ref(false)
const dialogs = ref<Dialog[]>([])
const docs = ref<Doc[]>([])
const results = ref<Result[]>([])
watchEffect(async () => {
try {
dialogs.value = global.value
? await db.dialogs.toArray()
: await db.dialogs.where('workspaceId').equals(props.workspaceId).toArray()
const messages = global.value
? await db.messages.toArray()
: dialogs.value.length
? await db.messages.where('dialogId').anyOf(dialogs.value.map(d => d.id)).toArray()
: []
docs.value = (messages || []).map(m => ({
id: m.id,
dialogId: m.dialogId,
content: (Array.isArray(m.contents) ? m.contents : [])
.filter(c => c && (c.type === 'assistant-message' || c.type === 'user-message'))
.map(c => typeof c.text === 'string' ? c.text : '')
.join('\n')
}))
search()
} catch (error) {
// Search is a sidebar utility and must never take down the whole SPA when
// an older/malformed IndexedDB message is encountered.
console.error('[SearchDialog] failed to load dialogs', error)
dialogs.value = []
docs.value = []
results.value = []
}
})

const q = ref('')
const listRef = ref<QList>()
function search() {
if (!q.value) return
const hits = docs.value.filter(d => caselessIncludes(d.content, q.value)).slice(0, 100)
if (!q.value || !docs.value || !dialogs.value) {
results.value = []
return
}
const hits = docs.value.filter(d => d && caselessIncludes(d.content || '', q.value)).slice(0, 100)
unmark()
results.value = [
...hits.map(h => {
const dialog = dialogs.value.find(d => d.id === h.dialogId)
return dialog && {
workspaceId: dialog.workspaceId,
dialogId: dialog.id,
title: dialog.name,
preview: h.content.match(new RegExp(`^.*${escapeRegex(q.value)}.*$`, 'im'))[0],
route: getRoute(dialog.msgTree, h.id)
title: dialog.name || '',
preview: h.content.match(new RegExp(`^.*${escapeRegex(q.value)}.*$`, 'im'))?.[0] || h.content.slice(0, 240),
route: getRoute(dialog.msgTree, h.id) || []
}
}).filter(Boolean),
...dialogs.value.filter(d => caselessIncludes(d.name, q.value)).map(d => ({
...dialogs.value.filter(d => caselessIncludes(d.name || '', q.value)).map(d => ({
workspaceId: d.workspaceId,
dialogId: d.id,
title: d.name,
title: d.name || '',
route: []
}))
].reverse()
Expand Down Expand Up @@ -179,10 +194,13 @@ watch(open, val => {
})
})

function getRoute(tree: Record<string, string[]>, target: string, curr = '$root') {
for (const [i, v] of tree[curr].entries()) {
function getRoute(tree: Record<string, string[]> | undefined, target: string, curr = '$root', seen = new Set<string>()): number[] | undefined {
if (!tree || seen.has(curr)) return
seen.add(curr)
const children = Array.isArray(tree[curr]) ? tree[curr] : []
for (const [i, v] of children.entries()) {
if (v === target) return [i]
const route = getRoute(tree, target, v)
const route = getRoute(tree, target, v, seen)
if (route) return [i, ...route]
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/ViewImageDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const imageStyle = computed(() => ({
}))

function onTouchStart(e: TouchEvent) {
if (e.touches.length === 0) return
if (e.touches.length === 2) {
wasMultiTouch = true
const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2
Expand All @@ -154,6 +155,7 @@ function onTouchStart(e: TouchEvent) {
}

function onTouchMove(e: TouchEvent) {
if (e.touches.length === 0) return
if (e.touches.length === 2) {
const dist = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
Expand Down
8 changes: 6 additions & 2 deletions src/composables/set-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { EdgeToEdge } from '@capawesome/capacitor-android-edge-to-edge-support'
let cachedSafeAreaTop = 0
let cachedSafeAreaBottom = 0


export function getSafeAreaTop(): number { return cachedSafeAreaTop }
export function getSafeAreaBottom(): number { return cachedSafeAreaBottom }

Expand Down Expand Up @@ -127,8 +128,11 @@ export function useSetTheme() {
cachedSafeAreaTop = 24
writeInsets()
})
Keyboard.addListener('keyboardWillShow', (info) => {
cachedSafeAreaBottom = info.keyboardHeight
Keyboard.addListener('keyboardDidShow', (info) => {
// In the current Android edge-to-edge window the WebView is not
// resized, so bottom: 0 is physically underneath the IME. The
// Capacitor Android value is native pixels; convert it to CSS px.
cachedSafeAreaBottom = info.keyboardHeight / Math.max(window.devicePixelRatio || 1, 1)
writeInsets()
})
Keyboard.addListener('keyboardWillHide', () => {
Expand Down
10 changes: 7 additions & 3 deletions src/composables/use-dialog-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ export function useDialogArtifact(
})
if (options.reserveOriginal) return
const to = `> ${t('dialogView.convertedToArtifact')}: <router-link to="?openArtifact=${id}">${name}</router-link>\n`
const index = message.contents.findIndex(c => ['assistant-message', 'user-message'].includes(c.type))
const content = message.contents[index] as UserMessageContent | AssistantMessageContent
const contents = message.contents || []
const index = contents.findIndex(c => ['assistant-message', 'user-message'].includes(c.type))
const content = contents[index] as UserMessageContent | AssistantMessageContent | undefined
if (!content) return
await db.messages.update(message.id, {
[`contents.${index}.text`]: content.text.replace(pattern, to) as any
})
Expand All @@ -62,7 +64,9 @@ export function useDialogArtifact(
const object: ExtractArtifactResult = JSON.parse(text)
if (!object.found) return
const reg = new RegExp(`(\`{3,}.*\\n)?(${object.regex})(\\s*\`{3,})?`)
const content = message.contents.find(c => c.type === 'assistant-message')
if (!message || !Array.isArray(message.contents)) return
const content = (message.contents || []).find(c => c.type === 'assistant-message') as AssistantMessageContent | undefined
if (!content || typeof content.text !== 'string') return
const match = content.text.match(reg)
if (!match) return
await extractArtifact(message, match[2], reg, {
Expand Down
2 changes: 1 addition & 1 deletion src/composables/use-dialog-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function useDialogBranch(

const parentId = chain.value[index - 1]
const messageId = chain.value[index]
const branches = dialog.value.msgTree[parentId]
const branches = dialog.value.msgTree[parentId] || []
if (!Array.isArray(branches) || branches.length <= 1) return null

const message = messageMap.value[messageId]
Expand Down
Loading