Skip to content

Conversation

Larch-C
Copy link

@Larch-C Larch-C commented Oct 6, 2025

fixes #XYZ


Motivation / 动机

目前 chat.vue 里面太臃肿了,于是进行解耦,使其更好维护

Modifications / 改动点

Verification Steps / 验证步骤

Screenshots or Test Results / 运行截图或测试结果

Compatibility & Breaking Changes / 兼容性与破坏性变更

  • 这是一个破坏性变更 (Breaking Change)。/ This is a breaking change.
  • 这不是一个破坏性变更。/ This is NOT a breaking change.

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Sourcery 总结

重构 ChatPage 组件,通过将其 UI 和逻辑解耦为可复用的子组件、服务模块和可组合工具

改进:

  • 将 Chat API 交互提取到一个专门的 services/chat.api.ts 模块中
  • 创建可组合函数(composables),用于流式传输 (useChatStream)、侧边栏状态 (useSidebarState)、路由同步 (useChatRouteSync)、媒体缓存 (useMediaCache) 和日期格式化 (useDateFormat)
  • 将单一的 Chat.vue 组件拆分为多个子组件:SidebarPanel, ConversationHeader, InputArea, WelcomePanel, EditTitleDialog, ImagePreviewDialog, and AttachmentsPreview
  • 将 Chat.vue 中的内联 CSS 移动到一个独立的 ChatPage.css 文件中,以实现集中式样式管理
Original summary in English

Summary by Sourcery

Refactor the ChatPage component by decoupling its UI and logic into reusable subcomponents, service modules, and composable utilities

Enhancements:

  • Extract Chat API interactions into a dedicated services/chat.api.ts module
  • Create composables for streaming (useChatStream), sidebar state (useSidebarState), route synchronization (useChatRouteSync), media caching (useMediaCache), and date formatting (useDateFormat)
  • Split the monolithic Chat.vue into multiple subcomponents: SidebarPanel, ConversationHeader, InputArea, WelcomePanel, EditTitleDialog, ImagePreviewDialog, and AttachmentsPreview
  • Move inline CSS from Chat.vue into a standalone ChatPage.css for centralized styling

@auto-assign auto-assign bot requested review from advent259141 and anka-afk October 6, 2025 10:57
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `dashboard/src/components/chat/InputArea.vue:98-104` </location>
<code_context>
+  }
+}
+
+async function startRecording() {
+  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+  mediaRecorder = new MediaRecorder(stream);
+  mediaRecorder.ondataavailable = (event) => {
+    audioChunks.push(event.data);
+  };
+  mediaRecorder.start();
+  isRecording.value = true;
+}
</code_context>

<issue_to_address>
**issue:** startRecording does not handle permission denial or errors from getUserMedia.

Add error handling to notify the user and maintain consistent UI state if getUserMedia fails or access is denied.
</issue_to_address>

### Comment 2
<location> `dashboard/src/components/chat/InputArea.vue:137-146` </location>
<code_context>
+  stagedImagesUrl.value.splice(index, 1);
+}
+
+function handlePaste(event: ClipboardEvent) {
+  const items = event.clipboardData?.items;
+  if (!items) return;
+  for (let i = 0; i < items.length; i++) {
+    if (items[i].type.indexOf('image') !== -1) {
+      const file = items[i].getAsFile();
+      if (file) processAndUploadImage(file);
+    }
+  }
+}
</code_context>

<issue_to_address>
**suggestion (bug_risk):** handlePaste does not prevent default paste behavior.

To avoid duplicate images or unexpected behavior, call event.preventDefault() when processing pasted images.

```suggestion
function handlePaste(event: ClipboardEvent) {
  event.preventDefault();
  const items = event.clipboardData?.items;
  if (!items) return;
  for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf('image') !== -1) {
      const file = items[i].getAsFile();
      if (file) processAndUploadImage(file);
    }
  }
}
```
</issue_to_address>

### Comment 3
<location> `dashboard/src/components/chat/SidebarPanel.vue:109-118` </location>
<code_context>
+
+    onUnmounted(() => dispose());
+
+    function formatDate(ts) {
+      // Sidebar 简化显示:当天仅显示时/分,否则显示月/日 时:分
+      if (!ts) return '';
+      const date = new Date(ts * 1000);
+      const now = new Date();
+      const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
+      if (date.getTime() < todayStart) {
+        return new Intl.DateTimeFormat(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(date);
+      }
+      return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date);
+    }
+
</code_context>

<issue_to_address>
**suggestion:** formatDate uses undefined locale for Intl.DateTimeFormat.

Defaulting to the browser locale may cause inconsistent date formats. Please use the application's configured locale for consistency.

Suggested implementation:

```
    // 假设 appLocale 是应用配置的 locale,可以通过 props、inject、或 composable 获取
    // 例如:const appLocale = inject('appLocale') || 'en-US';
    const appLocale = 'en-US'; // 请根据实际情况替换为应用的 locale

    function formatDate(ts) {
      // Sidebar 简化显示:当天仅显示时/分,否则显示月/日 时:分
      if (!ts) return '';
      const date = new Date(ts * 1000);
      const now = new Date();
      const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
      if (date.getTime() < todayStart) {
        return new Intl.DateTimeFormat(appLocale, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(date);
      }
      return new Intl.DateTimeFormat(appLocale, { hour: '2-digit', minute: '2-digit' }).format(date);
    }

```

- If your application already provides a locale (e.g., via Vue's i18n plugin, a global store, or context), replace `const appLocale = 'en-US'` with the correct way to access it.
- If `formatDate` is used outside the setup script, ensure `appLocale` is accessible in that scope.
</issue_to_address>

### Comment 4
<location> `dashboard/src/services/chat.api.ts:1` </location>
<code_context>

 <script>
 import { router } from '@/router';
-import axios from 'axios';
 import { ref } from 'vue';
 import { useCustomizerStore } from '@/stores/customizer';
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring repeated axios logic into a shared helper to simplify each API function.

Here’s a small refactoring that pulls out all the common bits into one helper and shrinks each endpoint down to just its unique bits:

```ts
// api.ts
import axios, { Method, AxiosRequestConfig } from 'axios';

const api = axios.create({ baseURL: '/api/chat' });

interface WrappedResponse<T> {
  data: T;
}

async function apiRequest<T>(
  method: Method,
  url: string,
  config: AxiosRequestConfig = {}
): Promise<T> {
  const res = await api.request<WrappedResponse<T>>({ method, url, ...config });
  return res.data.data;
}

export default apiRequest;
```

Then in your chat client file:

```ts
// chatApi.ts
import apiRequest from './api';

// Conversations
export const listConversations = () =>
  apiRequest<Conversation[]>('get', '/conversations');

export const getConversation = (conversation_id: string) =>
  apiRequest<Conversation>('get', '/get_conversation', {
    params: { conversation_id },
  });

export const newConversation = () =>
  apiRequest<Conversation>('get', '/new_conversation');

export const deleteConversation = (conversation_id: string) =>
  apiRequest<void>('get', '/delete_conversation', {
    params: { conversation_id },
  });

export const renameConversation = (conversation_id: string, title: string) =>
  apiRequest<void>('post', '/rename_conversation', {
    data: { conversation_id, title },
  });

// Media
const withForm = (url: string, file: File | Blob) => {
  const form = new FormData();
  form.append('file', file);
  return apiRequest<FileMeta>('post', url, {
    data: form,
    headers: { 'Content-Type': 'multipart/form-data' },
  });
};

export const postImage = (file: File) => withForm('/post_image', file);
export const postFile = (file: File | Blob) => withForm('/post_file', file);

export const getFile = (filename: string) =>
  apiRequest<Blob>('get', '/get_file', {
    params: { filename },
    responseType: 'blob',
  });

// keep your fetch-based streaming call as-is
export async function sendMessageStream(payload: { /* ... */ }) {
  /* unchanged */
}
```

Benefits:

- One place to configure baseURL, error handling, and `.data?.data` extraction.
- Each exported function is now 1–2 lines of its unique logic.
- Adding headers, responseType, params, form data is all still possible via the `config` parameter.
</issue_to_address>

### Comment 5
<location> `dashboard/src/composables/chat/useChatStream.ts:11` </location>
<code_context>
+};
+
+export function useChatStream(getMediaUrl: (filename: string) => Promise<string>) {
+  async function runStream(body: ReadableStream<Uint8Array> | null, handlers: ChatStreamHandlers = {}) {
+    if (!body) return;
+    const reader = body.getReader();
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring runStream by extracting JSON parsing and type dispatch into helper functions for improved clarity and maintainability.

```markdown
You can flatten `runStream` by pulling out the JSON-parsing and type-dispatch logic into small helpers. For example:

```ts
type StreamChunk = {
  type: string
  data: any
  cid?: string
  streaming?: boolean
}

async function parseChunk(line: string): Promise<StreamChunk | null> {
  try {
    const raw = line.replace(/^data:\s*/, '')
    const obj = JSON.parse(raw)
    if (obj && typeof obj === 'object' && 'type' in obj) return obj
  } catch {
    console.warn('failed to parse', line)
  }
  return null
}

function makeDispatch(
  getMediaUrl: (f: string) => Promise<string>,
  handlers: ChatStreamHandlers
) {
  let inStreaming = false

  const map: Record<string, (c: StreamChunk) => Promise<void>> = {
    error: async c => handlers.onError?.(c.data),
    image: async c => {
      const url = await getMediaUrl(String(c.data).replace('[IMAGE]', ''))
      handlers.onImage?.(url)
    },
    record: async c => {
      const url = await getMediaUrl(String(c.data).replace('[RECORD]', ''))
      handlers.onAudio?.(url)
    },
    plain: async c => {
      if (!inStreaming) {
        handlers.onTextStart?.(c.data)
        inStreaming = true
      } else {
        handlers.onTextAppend?.(c.data)
      }
    },
    update_title: async c => handlers.onUpdateTitle?.(c.cid!, c.data),
  }

  return async (chunk: StreamChunk) => {
    await (map[chunk.type] ?? (async () => {}))(chunk)
    if ((chunk.type === 'break' && chunk.streaming) || !chunk.streaming) {
      inStreaming = false
    }
  }
}
```

Then your `runStream` becomes:

```ts
async function runStream(
  body: ReadableStream<Uint8Array> | null,
  handlers: ChatStreamHandlers = {}
) {
  if (!body) return
  const reader = body.getReader()
  const decoder = new TextDecoder()
  const dispatch = makeDispatch(getMediaUrl, handlers)

  while (true) {
    try {
      const { done, value } = await reader.read()
      if (done) break
      const chunk = decoder.decode(value, { stream: true })
      for (const line of chunk.split('\n\n')) {
        const trimmed = line.trim()
        if (!trimmed) continue
        const data = await parseChunk(trimmed)
        if (data) await dispatch(data)
      }
    } catch (err) {
      handlers.onError?.(err)
      break
    }
  }
}
```

This splits responsibilities into:
1. `parseChunk`JSON parsing & validation  
2. `makeDispatch` – a flat map of type-handlers  
3. `runStream` – streaming loop only  
which should be much easier to read and test.
</issue_to_address>

### Comment 6
<location> `dashboard/src/composables/chat/useSidebarState.ts:6` </location>
<code_context>
+const LOCAL_KEY = 'sidebarCollapsed';
+
+export function useSidebarState() {
+  const sidebarCollapsed = ref(true);
+  const sidebarHovered = ref(false);
+  const sidebarHoverExpanded = ref(false);
</code_context>

<issue_to_address>
**issue (complexity):** Consider consolidating multiple sidebar state refs into a single reactive object and using local variables for timers.

```suggestion
// Instead of 5 separate refs (`sidebarCollapsed`, `sidebarHovered`, 
// `sidebarHoverExpanded`, `sidebarHoverTimer`, `sidebarHoverDelay`), 
// you can collapse them into one reactive object + a local timer variable.

import { reactive, toRefs, watch, onBeforeUnmount } from 'vue'

const LOCAL_KEY = 'sidebarCollapsed'
const HOVER_DELAY = 100

export function useSidebarState() {
  // 1. single state object
  const state = reactive({
    collapsed: JSON.parse(localStorage.getItem(LOCAL_KEY) ?? 'true'),
    hovered: false,
    hoverExpanded: false,
  })

  // 2. local timer var (no need for a ref)
  let hoverTimer: number

  // 3. persist whenever `collapsed` changes
  watch(() => state.collapsed, v =>
    localStorage.setItem(LOCAL_KEY, JSON.stringify(v))
  )

  function toggleSidebar() {
    if (state.hoverExpanded) {
      state.hoverExpanded = false
    } else {
      state.collapsed = !state.collapsed
    }
  }

  function handleSidebarMouseEnter() {
    if (!state.collapsed) return
    state.hovered = true
    hoverTimer = window.setTimeout(() => {
      if (state.hovered) {
        state.hoverExpanded = true
        state.collapsed = false
      }
    }, HOVER_DELAY)
  }

  function handleSidebarMouseLeave() {
    state.hovered = false
    clearTimeout(hoverTimer)
    if (state.hoverExpanded) {
      state.collapsed = true
      state.hoverExpanded = false
    }
  }

  onBeforeUnmount(() => clearTimeout(hoverTimer))

  // expose individual properties if you still need them
  return {
    ...toRefs(state),
    toggleSidebar,
    handleSidebarMouseEnter,
    handleSidebarMouseLeave,
  }
}
```

Benefits:
1. State is grouped into one `reactive` object.
2. `hoverTimer` and `HOVER_DELAY` are simple locals (no extra refs).
3. `collapsed` persistence is handled declaratively with a `watch`.
4. Functionality remains identical.
</issue_to_address>

### Comment 7
<location> `dashboard/src/composables/chat/useChatRouteSync.ts:4-31` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 8
<location> `dashboard/src/composables/chat/useChatRouteSync.ts:33-58` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 9
<location> `dashboard/src/composables/chat/useChatStream.ts:11-75` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 10
<location> `dashboard/src/composables/chat/useChatStream.ts:41` </location>
<code_context>
          const type = chunk_json.type;

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
          const {type} = chunk_json;
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### Comment 11
<location> `dashboard/src/composables/chat/useMediaCache.ts:4-10` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 12
<location> `dashboard/src/composables/chat/useMediaCache.ts:12-17` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 13
<location> `dashboard/src/composables/chat/useSidebarState.ts:16-18` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 14
<location> `dashboard/src/composables/chat/useSidebarState.ts:20-27` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 15
<location> `dashboard/src/composables/chat/useSidebarState.ts:29-38` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 16
<location> `dashboard/src/composables/chat/useSidebarState.ts:40-50` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 17
<location> `dashboard/src/composables/chat/useSidebarState.ts:52-54` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +98 to +104
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.start();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: startRecording does not handle permission denial or errors from getUserMedia.

Add error handling to notify the user and maintain consistent UI state if getUserMedia fails or access is denied.

Comment on lines +137 to +146
function handlePaste(event: ClipboardEvent) {
const items = event.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile();
if (file) processAndUploadImage(file);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): handlePaste does not prevent default paste behavior.

To avoid duplicate images or unexpected behavior, call event.preventDefault() when processing pasted images.

Suggested change
function handlePaste(event: ClipboardEvent) {
const items = event.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile();
if (file) processAndUploadImage(file);
}
}
}
function handlePaste(event: ClipboardEvent) {
event.preventDefault();
const items = event.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile();
if (file) processAndUploadImage(file);
}
}
}

Comment on lines +109 to +118
function formatDate(ts) {
// Sidebar 简化显示:当天仅显示时/分,否则显示月/日 时:分
if (!ts) return '';
const date = new Date(ts * 1000);
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
if (date.getTime() < todayStart) {
return new Intl.DateTimeFormat(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(date);
}
return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: formatDate uses undefined locale for Intl.DateTimeFormat.

Defaulting to the browser locale may cause inconsistent date formats. Please use the application's configured locale for consistency.

Suggested implementation:

    // 假设 appLocale 是应用配置的 locale,可以通过 props、inject、或 composable 获取
    // 例如:const appLocale = inject('appLocale') || 'en-US';
    const appLocale = 'en-US'; // 请根据实际情况替换为应用的 locale

    function formatDate(ts) {
      // Sidebar 简化显示:当天仅显示时/分,否则显示月/日 时:分
      if (!ts) return '';
      const date = new Date(ts * 1000);
      const now = new Date();
      const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
      if (date.getTime() < todayStart) {
        return new Intl.DateTimeFormat(appLocale, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(date);
      }
      return new Intl.DateTimeFormat(appLocale, { hour: '2-digit', minute: '2-digit' }).format(date);
    }

  • If your application already provides a locale (e.g., via Vue's i18n plugin, a global store, or context), replace const appLocale = 'en-US' with the correct way to access it.
  • If formatDate is used outside the setup script, ensure appLocale is accessible in that scope.

@@ -0,0 +1,89 @@
import axios from 'axios';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring repeated axios logic into a shared helper to simplify each API function.

Here’s a small refactoring that pulls out all the common bits into one helper and shrinks each endpoint down to just its unique bits:

// api.ts
import axios, { Method, AxiosRequestConfig } from 'axios';

const api = axios.create({ baseURL: '/api/chat' });

interface WrappedResponse<T> {
  data: T;
}

async function apiRequest<T>(
  method: Method,
  url: string,
  config: AxiosRequestConfig = {}
): Promise<T> {
  const res = await api.request<WrappedResponse<T>>({ method, url, ...config });
  return res.data.data;
}

export default apiRequest;

Then in your chat client file:

// chatApi.ts
import apiRequest from './api';

// Conversations
export const listConversations = () =>
  apiRequest<Conversation[]>('get', '/conversations');

export const getConversation = (conversation_id: string) =>
  apiRequest<Conversation>('get', '/get_conversation', {
    params: { conversation_id },
  });

export const newConversation = () =>
  apiRequest<Conversation>('get', '/new_conversation');

export const deleteConversation = (conversation_id: string) =>
  apiRequest<void>('get', '/delete_conversation', {
    params: { conversation_id },
  });

export const renameConversation = (conversation_id: string, title: string) =>
  apiRequest<void>('post', '/rename_conversation', {
    data: { conversation_id, title },
  });

// Media
const withForm = (url: string, file: File | Blob) => {
  const form = new FormData();
  form.append('file', file);
  return apiRequest<FileMeta>('post', url, {
    data: form,
    headers: { 'Content-Type': 'multipart/form-data' },
  });
};

export const postImage = (file: File) => withForm('/post_image', file);
export const postFile = (file: File | Blob) => withForm('/post_file', file);

export const getFile = (filename: string) =>
  apiRequest<Blob>('get', '/get_file', {
    params: { filename },
    responseType: 'blob',
  });

// keep your fetch-based streaming call as-is
export async function sendMessageStream(payload: { /* ... */ }) {
  /* unchanged */
}

Benefits:

  • One place to configure baseURL, error handling, and .data?.data extraction.
  • Each exported function is now 1–2 lines of its unique logic.
  • Adding headers, responseType, params, form data is all still possible via the config parameter.

};

export function useChatStream(getMediaUrl: (filename: string) => Promise<string>) {
async function runStream(body: ReadableStream<Uint8Array> | null, handlers: ChatStreamHandlers = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring runStream by extracting JSON parsing and type dispatch into helper functions for improved clarity and maintainability.

You can flatten `runStream` by pulling out the JSON-parsing and type-dispatch logic into small helpers. For example:

```ts
type StreamChunk = {
  type: string
  data: any
  cid?: string
  streaming?: boolean
}

async function parseChunk(line: string): Promise<StreamChunk | null> {
  try {
    const raw = line.replace(/^data:\s*/, '')
    const obj = JSON.parse(raw)
    if (obj && typeof obj === 'object' && 'type' in obj) return obj
  } catch {
    console.warn('failed to parse', line)
  }
  return null
}

function makeDispatch(
  getMediaUrl: (f: string) => Promise<string>,
  handlers: ChatStreamHandlers
) {
  let inStreaming = false

  const map: Record<string, (c: StreamChunk) => Promise<void>> = {
    error: async c => handlers.onError?.(c.data),
    image: async c => {
      const url = await getMediaUrl(String(c.data).replace('[IMAGE]', ''))
      handlers.onImage?.(url)
    },
    record: async c => {
      const url = await getMediaUrl(String(c.data).replace('[RECORD]', ''))
      handlers.onAudio?.(url)
    },
    plain: async c => {
      if (!inStreaming) {
        handlers.onTextStart?.(c.data)
        inStreaming = true
      } else {
        handlers.onTextAppend?.(c.data)
      }
    },
    update_title: async c => handlers.onUpdateTitle?.(c.cid!, c.data),
  }

  return async (chunk: StreamChunk) => {
    await (map[chunk.type] ?? (async () => {}))(chunk)
    if ((chunk.type === 'break' && chunk.streaming) || !chunk.streaming) {
      inStreaming = false
    }
  }
}

Then your runStream becomes:

async function runStream(
  body: ReadableStream<Uint8Array> | null,
  handlers: ChatStreamHandlers = {}
) {
  if (!body) return
  const reader = body.getReader()
  const decoder = new TextDecoder()
  const dispatch = makeDispatch(getMediaUrl, handlers)

  while (true) {
    try {
      const { done, value } = await reader.read()
      if (done) break
      const chunk = decoder.decode(value, { stream: true })
      for (const line of chunk.split('\n\n')) {
        const trimmed = line.trim()
        if (!trimmed) continue
        const data = await parseChunk(trimmed)
        if (data) await dispatch(data)
      }
    } catch (err) {
      handlers.onError?.(err)
      break
    }
  }
}

This splits responsibilities into:

  1. parseChunk – JSON parsing & validation
  2. makeDispatch – a flat map of type-handlers
  3. runStream – streaming loop only
    which should be much easier to read and test.

const LOCAL_KEY = 'sidebarCollapsed';

export function useSidebarState() {
const sidebarCollapsed = ref(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider consolidating multiple sidebar state refs into a single reactive object and using local variables for timers.

Suggested change
const sidebarCollapsed = ref(true);
// Instead of 5 separate refs (`sidebarCollapsed`, `sidebarHovered`,
// `sidebarHoverExpanded`, `sidebarHoverTimer`, `sidebarHoverDelay`),
// you can collapse them into one reactive object + a local timer variable.
import { reactive, toRefs, watch, onBeforeUnmount } from 'vue'
const LOCAL_KEY = 'sidebarCollapsed'
const HOVER_DELAY = 100
export function useSidebarState() {
// 1. single state object
const state = reactive({
collapsed: JSON.parse(localStorage.getItem(LOCAL_KEY) ?? 'true'),
hovered: false,
hoverExpanded: false,
})
// 2. local timer var (no need for a ref)
let hoverTimer: number
// 3. persist whenever `collapsed` changes
watch(() => state.collapsed, v =>
localStorage.setItem(LOCAL_KEY, JSON.stringify(v))
)
function toggleSidebar() {
if (state.hoverExpanded) {
state.hoverExpanded = false
} else {
state.collapsed = !state.collapsed
}
}
function handleSidebarMouseEnter() {
if (!state.collapsed) return
state.hovered = true
hoverTimer = window.setTimeout(() => {
if (state.hovered) {
state.hoverExpanded = true
state.collapsed = false
}
}, HOVER_DELAY)
}
function handleSidebarMouseLeave() {
state.hovered = false
clearTimeout(hoverTimer)
if (state.hoverExpanded) {
state.collapsed = true
state.hoverExpanded = false
}
}
onBeforeUnmount(() => clearTimeout(hoverTimer))
// expose individual properties if you still need them
return {
...toRefs(state),
toggleSidebar,
handleSidebarMouseEnter,
handleSidebarMouseLeave,
}
}

Benefits:

  1. State is grouped into one reactive object.
  2. hoverTimer and HOVER_DELAY are simple locals (no extra refs).
  3. collapsed persistence is handled declaratively with a watch.
  4. Functionality remains identical.

continue;
}

const type = chunk_json.type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Prefer object destructuring when accessing and using properties. (use-object-destructuring)

Suggested change
const type = chunk_json.type;
const {type} = chunk_json;


ExplanationObject destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the Airbnb Javascript Style Guide

@Larch-C Larch-C closed this Oct 8, 2025
@Soulter
Copy link
Member

Soulter commented Oct 8, 2025

怎么关掉了(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants