Initial commit: AI chat assistant with workflow chat, workspace, and profile tabs

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:23:00 +08:00
commit 3fc394921e
34 changed files with 3449 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
<template>
<view
class="message-bubble"
:class="{
'message-bubble--user': message.role === 'user',
'message-bubble--ai': message.role === 'assistant',
'message-bubble--error': message.status === 'error',
}"
>
<!-- AI Avatar placeholder -->
<view v-if="message.role === 'assistant'" class="message-bubble__avatar">
<text class="message-bubble__avatar-text">AI</text>
</view>
<view class="message-bubble__content-wrapper">
<!-- Bubble -->
<view class="message-bubble__bubble">
<!-- Parsed content segments -->
<view v-for="(segment, idx) in parsedSegments" :key="idx">
<!-- Code block -->
<view v-if="segment.type === 'code'" class="message-bubble__code-block">
<text class="message-bubble__code-text">{{ segment.value }}</text>
</view>
<!-- Regular text (with inline bold handled) -->
<text v-else class="message-bubble__text">{{ segment.value }}</text>
</view>
</view>
<!-- Timestamp & Status -->
<view class="message-bubble__meta" :class="{ 'message-bubble__meta--user': message.role === 'user' }">
<text class="message-bubble__time">{{ formatTime(message.timestamp) }}</text>
<text v-if="message.role === 'user' && message.status === 'sending'" class="message-bubble__status">⏳</text>
<text v-if="message.role === 'user' && message.status === 'sent'" class="message-bubble__status">✓</text>
<text v-if="message.role === 'user' && message.status === 'error'" class="message-bubble__status message-bubble__status--error">⚠️</text>
</view>
<!-- Error retry -->
<view v-if="message.status === 'error'" class="message-bubble__retry" @tap="onRetry">
<text class="message-bubble__retry-text">发送失败,点击重试</text>
</view>
</view>
<!-- User Avatar placeholder -->
<view v-if="message.role === 'user'" class="message-bubble__avatar message-bubble__avatar--user">
<text class="message-bubble__avatar-text">我</text>
</view>
</view>
</template>
<script setup lang="uts">
import type { Message, ParsedSegment } from '@/types/index'
const props = defineProps<{
message: Message
}>()
const emit = defineEmits<{
retry: []
}>()
/**
* Parse message content into segments: text, code, bold.
* Handles ``` code fences and **bold** markers.
*/
const parsedSegments = computed<ParsedSegment[]>(() => {
return parseContent(props.message.content)
})
function parseContent(content: string): ParsedSegment[] {
const segments: ParsedSegment[] = []
let remaining = content
while (remaining.length > 0) {
const codeStart = remaining.indexOf('```')
if (codeStart === -1) {
// No more code blocks — add remaining as text
if (remaining.trim()) {
segments.push({ type: 'text', value: remaining.trim() })
}
break
}
// Text before code block
if (codeStart > 0) {
const beforeText = remaining.substring(0, codeStart).trim()
if (beforeText) {
segments.push({ type: 'text', value: beforeText })
}
}
// Find code block end
const afterFence = remaining.substring(codeStart + 3)
const codeEnd = afterFence.indexOf('```')
if (codeEnd === -1) {
// Unclosed code block — treat rest as text
segments.push({ type: 'text', value: remaining.substring(codeStart).trim() })
break
}
const codeContent = afterFence.substring(0, codeEnd).trim()
if (codeContent) {
segments.push({ type: 'code', value: codeContent })
}
remaining = afterFence.substring(codeEnd + 3)
}
// If no segments at all, return the whole content as text
if (segments.length === 0 && content.trim()) {
segments.push({ type: 'text', value: content.trim() })
}
return segments
}
/**
* Format timestamp to HH:mm.
*/
function formatTime(timestamp: number): string {
const d = new Date(timestamp)
const h = d.getHours().toString().padStart(2, '0')
const m = d.getMinutes().toString().padStart(2, '0')
return `${h}:${m}`
}
function onRetry(): void {
emit('retry')
}
</script>
<style scoped>
.message-bubble {
display: flex;
flex-direction: row;
align-items: flex-end;
margin-bottom: 20rpx;
padding: 0 24rpx;
animation: messageEnter 0.3s ease-out;
}
.message-bubble--user {
justify-content: flex-end;
}
.message-bubble--ai {
justify-content: flex-start;
}
/* Avatar */
.message-bubble__avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 12rpx;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
}
.message-bubble__avatar--user {
margin-right: 0;
margin-left: 12rpx;
background: linear-gradient(135deg, #00b894, #55efc4);
}
.message-bubble__avatar-text {
font-size: 22rpx;
font-weight: 700;
color: #ffffff;
}
/* Content wrapper */
.message-bubble__content-wrapper {
display: flex;
flex-direction: column;
max-width: 80%;
}
.message-bubble--ai .message-bubble__content-wrapper {
max-width: 78%;
}
/* Bubble box */
.message-bubble__bubble {
padding: 20rpx 24rpx;
border-radius: 24rpx 24rpx 4rpx 24rpx;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.message-bubble--user .message-bubble__bubble {
border-radius: 24rpx 24rpx 24rpx 4rpx;
background: linear-gradient(135deg, #6c5ce7 0%, #5a4bd1 100%);
border: none;
}
.message-bubble--error .message-bubble__bubble {
border-color: rgba(255, 82, 82, 0.4);
}
/* Text */
.message-bubble__text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
word-break: break-word;
}
.message-bubble--ai .message-bubble__text {
color: rgba(255, 255, 255, 0.88);
}
.message-bubble--user .message-bubble__text {
color: #ffffff;
}
/* Code block */
.message-bubble__code-block {
background: rgba(0, 0, 0, 0.35);
border-radius: 12rpx;
padding: 16rpx 20rpx;
margin: 12rpx 0;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.message-bubble__code-text {
font-size: 24rpx;
font-family: 'Courier New', Courier, monospace;
color: #a29bfe;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
/* Meta (time + status) */
.message-bubble__meta {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 6rpx;
padding: 0 8rpx;
}
.message-bubble__meta--user {
justify-content: flex-end;
}
.message-bubble__time {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.3);
}
.message-bubble__status {
font-size: 18rpx;
margin-left: 8rpx;
color: rgba(255, 255, 255, 0.4);
}
.message-bubble__status--error {
color: #ff5252;
}
/* Retry */
.message-bubble__retry {
margin-top: 8rpx;
padding: 8rpx 16rpx;
align-self: flex-end;
}
.message-bubble__retry-text {
font-size: 22rpx;
color: #ff5252;
}
/* Enter animation */
@keyframes messageEnter {
from {
opacity: 0;
transform: translateY(16rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>