修改前端
This commit is contained in:
294
src/App.vue
294
src/App.vue
@@ -1,5 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import baiduMark from './assets/baidu-cloud-mark.svg'
|
||||||
|
import baiduChatMark from './assets/baidu2.png'
|
||||||
|
|
||||||
const apiBase = ref(import.meta.env.VITE_API_BASE || 'http://127.0.0.1:9000')
|
const apiBase = ref(import.meta.env.VITE_API_BASE || 'http://127.0.0.1:9000')
|
||||||
const authBase = ref(import.meta.env.VITE_AUTH_BASE || apiBase.value)
|
const authBase = ref(import.meta.env.VITE_AUTH_BASE || apiBase.value)
|
||||||
@@ -18,10 +20,13 @@ const registerPending = ref(false)
|
|||||||
const registerRequestId = ref(0)
|
const registerRequestId = ref(0)
|
||||||
const registerStatusMessage = ref('')
|
const registerStatusMessage = ref('')
|
||||||
const codeCountdown = ref(0)
|
const codeCountdown = ref(0)
|
||||||
|
const hasSentVerificationCode = ref(false)
|
||||||
|
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const messages = ref([])
|
const messages = ref([])
|
||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
|
const assistantThinking = ref(false)
|
||||||
|
const assistantBaselineCount = ref(0)
|
||||||
const streamState = ref('disconnected')
|
const streamState = ref('disconnected')
|
||||||
const messagesEl = ref(null)
|
const messagesEl = ref(null)
|
||||||
|
|
||||||
@@ -60,6 +65,19 @@ function pushMessage(role, content, kind = 'message') {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countAssistantMessages(list) {
|
||||||
|
return list.filter((m) => m.role === 'assistant' && m.kind !== 'error').length
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAssistantThinking() {
|
||||||
|
assistantBaselineCount.value = countAssistantMessages(messages.value)
|
||||||
|
assistantThinking.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAssistantThinking() {
|
||||||
|
assistantThinking.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function scrollToBottom(smooth = true) {
|
function scrollToBottom(smooth = true) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (!messagesEl.value) return
|
if (!messagesEl.value) return
|
||||||
@@ -113,6 +131,23 @@ function clearSession() {
|
|||||||
registerStatusMessage.value = ''
|
registerStatusMessage.value = ''
|
||||||
codeCountdown.value = 0
|
codeCountdown.value = 0
|
||||||
streamState.value = 'disconnected'
|
streamState.value = 'disconnected'
|
||||||
|
assistantThinking.value = false
|
||||||
|
assistantBaselineCount.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchAuthMode(mode) {
|
||||||
|
authMode.value = mode
|
||||||
|
authError.value = ''
|
||||||
|
registerStatusMessage.value = ''
|
||||||
|
registerPending.value = false
|
||||||
|
if (mode === 'login') {
|
||||||
|
hasSentVerificationCode.value = false
|
||||||
|
codeCountdown.value = 0
|
||||||
|
if (codeCountdownTimer) {
|
||||||
|
clearInterval(codeCountdownTimer)
|
||||||
|
codeCountdownTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCodeCountdown() {
|
function startCodeCountdown() {
|
||||||
@@ -120,7 +155,7 @@ function startCodeCountdown() {
|
|||||||
clearInterval(codeCountdownTimer)
|
clearInterval(codeCountdownTimer)
|
||||||
codeCountdownTimer = null
|
codeCountdownTimer = null
|
||||||
}
|
}
|
||||||
codeCountdown.value = 120
|
codeCountdown.value = 180
|
||||||
codeCountdownTimer = setInterval(() => {
|
codeCountdownTimer = setInterval(() => {
|
||||||
if (codeCountdown.value <= 1) {
|
if (codeCountdown.value <= 1) {
|
||||||
codeCountdown.value = 0
|
codeCountdown.value = 0
|
||||||
@@ -134,6 +169,45 @@ function startCodeCountdown() {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendVerificationCode() {
|
||||||
|
const phone = loginPhone.value.trim()
|
||||||
|
const password = loginPassword.value
|
||||||
|
if (!/^\d{6,20}$/.test(phone)) {
|
||||||
|
authError.value = '请先输入有效手机号'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
authError.value = '请先输入密码'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localAuthOnly.value) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${authBase.value}/auth/send-code`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phone, password }),
|
||||||
|
})
|
||||||
|
const payload = await resp.json().catch(() => ({}))
|
||||||
|
if (!resp.ok || !payload.ok) {
|
||||||
|
authError.value = payload.detail || payload.error || '验证码发送失败'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registerStatusMessage.value = payload.message || '验证码已发送,请在 180 秒内完成注册'
|
||||||
|
} catch (err) {
|
||||||
|
authError.value = String(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authError.value = ''
|
||||||
|
hasSentVerificationCode.value = true
|
||||||
|
if (localAuthOnly.value) {
|
||||||
|
registerStatusMessage.value = '验证码已发送,请在 180 秒内完成注册'
|
||||||
|
}
|
||||||
|
startCodeCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
function startRegisterStatusPolling() {
|
function startRegisterStatusPolling() {
|
||||||
if (registerPollTimer) {
|
if (registerPollTimer) {
|
||||||
clearInterval(registerPollTimer)
|
clearInterval(registerPollTimer)
|
||||||
@@ -258,6 +332,11 @@ async function register() {
|
|||||||
authLoading.value = true
|
authLoading.value = true
|
||||||
authError.value = ''
|
authError.value = ''
|
||||||
registerStatusMessage.value = ''
|
registerStatusMessage.value = ''
|
||||||
|
if (!hasSentVerificationCode.value) {
|
||||||
|
authError.value = '请先点击“发送”获取验证码'
|
||||||
|
authLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${authBase.value}/auth/register`, {
|
const resp = await fetch(`${authBase.value}/auth/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -277,7 +356,7 @@ async function register() {
|
|||||||
registerPending.value = true
|
registerPending.value = true
|
||||||
registerRequestId.value = Number(payload.request_id || 0)
|
registerRequestId.value = Number(payload.request_id || 0)
|
||||||
registerStatusMessage.value = '待后端验证'
|
registerStatusMessage.value = '待后端验证'
|
||||||
startCodeCountdown()
|
hasSentVerificationCode.value = false
|
||||||
startRegisterStatusPolling()
|
startRegisterStatusPolling()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authError.value = String(err)
|
authError.value = String(err)
|
||||||
@@ -361,6 +440,9 @@ async function loadHistory() {
|
|||||||
seenMessages.add(messageSignature(m.role, m.content, m.kind, m.at))
|
seenMessages.add(messageSignature(m.role, m.content, m.kind, m.at))
|
||||||
}
|
}
|
||||||
messages.value = nextMessages
|
messages.value = nextMessages
|
||||||
|
if (assistantThinking.value && countAssistantMessages(nextMessages) > assistantBaselineCount.value) {
|
||||||
|
stopAssistantThinking()
|
||||||
|
}
|
||||||
scrollToBottom(false)
|
scrollToBottom(false)
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -401,6 +483,9 @@ function connectStream() {
|
|||||||
kind,
|
kind,
|
||||||
at,
|
at,
|
||||||
})
|
})
|
||||||
|
if (role === 'assistant') {
|
||||||
|
stopAssistantThinking()
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Ignore malformed payload and keep the stream alive.
|
// Ignore malformed payload and keep the stream alive.
|
||||||
}
|
}
|
||||||
@@ -431,13 +516,16 @@ async function sendMessage() {
|
|||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const detail = await resp.text()
|
const detail = await resp.text()
|
||||||
|
stopAssistantThinking()
|
||||||
pushMessage('system', `Send failed: ${detail || resp.statusText}`, 'error')
|
pushMessage('system', `Send failed: ${detail || resp.statusText}`, 'error')
|
||||||
} else {
|
} else {
|
||||||
|
startAssistantThinking()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadHistory()
|
loadHistory()
|
||||||
}, 800)
|
}, 800)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
stopAssistantThinking()
|
||||||
pushMessage('system', `Send failed: ${String(err)}`, 'error')
|
pushMessage('system', `Send failed: ${String(err)}`, 'error')
|
||||||
} finally {
|
} finally {
|
||||||
text.value = ''
|
text.value = ''
|
||||||
@@ -458,6 +546,15 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => assistantThinking.value,
|
||||||
|
(thinking) => {
|
||||||
|
if (thinking) {
|
||||||
|
scrollToBottom(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (eventSource) eventSource.close()
|
if (eventSource) eventSource.close()
|
||||||
if (historyPollTimer) clearInterval(historyPollTimer)
|
if (historyPollTimer) clearInterval(historyPollTimer)
|
||||||
@@ -470,112 +567,133 @@ onBeforeUnmount(() => {
|
|||||||
<main class="screen">
|
<main class="screen">
|
||||||
<section v-if="!isLoggedIn" class="auth-shell">
|
<section v-if="!isLoggedIn" class="auth-shell">
|
||||||
<header class="top-bar">
|
<header class="top-bar">
|
||||||
<div>
|
<div class="auth-headline">
|
||||||
<h1>nanobot login</h1>
|
<img class="auth-logo" :src="baiduMark" alt="Baidu AI+" />
|
||||||
<p>Use phone + password to access your chat workspace</p>
|
<div>
|
||||||
|
<h1>欢迎使用百度 AI+ 教育智能助手</h1>
|
||||||
|
<p>请输入手机号和密码登录,首次注册需输入验证码并验证</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
<label>
|
<template v-if="authMode === 'login'">
|
||||||
phone
|
<label>
|
||||||
<input v-model="loginPhone" placeholder="e.g. 13800000000" />
|
手机号
|
||||||
</label>
|
<input v-model="loginPhone" placeholder="请输入手机号,例如 13800000000" />
|
||||||
<label>
|
</label>
|
||||||
password
|
<label>
|
||||||
<input v-model="loginPassword" type="password" placeholder="at least 6 chars" />
|
密码
|
||||||
</label>
|
<input v-model="loginPassword" type="password" placeholder="请输入登录密码" />
|
||||||
<label v-if="!localAuthOnly">
|
</label>
|
||||||
verification code
|
</template>
|
||||||
<input v-model="verificationCode" placeholder="请输入验证码" />
|
|
||||||
</label>
|
<template v-else>
|
||||||
|
<label>
|
||||||
|
手机号
|
||||||
|
<input v-model="loginPhone" placeholder="请输入手机号,例如 13800000000" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
密码
|
||||||
|
<input v-model="loginPassword" type="password" placeholder="请设置登录密码" />
|
||||||
|
</label>
|
||||||
|
<label v-if="!localAuthOnly">
|
||||||
|
验证码
|
||||||
|
<div class="inline-field">
|
||||||
|
<input v-model="verificationCode" placeholder="请输入验证码" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="send-code-btn"
|
||||||
|
:disabled="codeCountdown > 0 || authLoading"
|
||||||
|
@click="sendVerificationCode"
|
||||||
|
>
|
||||||
|
{{ codeCountdown > 0 ? `${codeCountdown}s` : '发送' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="ai-tags">
|
||||||
|
<span class="ai-tag">百度 AI+</span>
|
||||||
|
<span class="ai-tag">智能问答</span>
|
||||||
|
<span class="ai-tag">实时会话</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="auth-actions">
|
<div class="auth-actions">
|
||||||
<button :disabled="authLoading" @click="login">Login</button>
|
<button v-if="authMode === 'login'" :disabled="authLoading" @click="login">登录</button>
|
||||||
<button class="reconnect" :disabled="authLoading" @click="register">Register</button>
|
<button v-if="authMode === 'register'" :disabled="authLoading || registerPending" @click="register">提交注册</button>
|
||||||
|
<button
|
||||||
|
v-if="authMode === 'login'"
|
||||||
|
type="button"
|
||||||
|
class="reconnect"
|
||||||
|
:disabled="authLoading"
|
||||||
|
@click="switchAuthMode('register')"
|
||||||
|
>
|
||||||
|
注册账号
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="authMode === 'register'"
|
||||||
|
type="button"
|
||||||
|
class="reconnect"
|
||||||
|
:disabled="authLoading"
|
||||||
|
@click="switchAuthMode('login')"
|
||||||
|
>
|
||||||
|
返回登录
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="auth-error" v-if="authError">{{ authError }}</p>
|
<p class="auth-error" v-if="authError">{{ authError }}</p>
|
||||||
<p class="auth-error" v-if="registerPending || registerStatusMessage">
|
<p class="auth-error" v-if="registerPending || registerStatusMessage">
|
||||||
{{ registerStatusMessage || '待后端验证' }}
|
{{ registerStatusMessage || '请等待验证' }}
|
||||||
</p>
|
|
||||||
<p class="auth-error" v-if="registerPending && codeCountdown > 0">
|
|
||||||
验证码倒计时:{{ codeCountdown }}s
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-else class="chat-shell">
|
<section v-else class="chat-shell">
|
||||||
<div class="workspace">
|
<section class="workspace-main">
|
||||||
<aside class="workspace-sidebar">
|
<header class="main-head">
|
||||||
<div class="sidebar-brand">
|
<div class="main-head-brand">
|
||||||
<h2>nanobot</h2>
|
<img class="chat-logo" :src="baiduChatMark" alt="Baidu" />
|
||||||
<p>AI workspace</p>
|
<div>
|
||||||
</div>
|
<h1>百度 AI+ 教育智能助手</h1>
|
||||||
|
<p>欢迎与我实时交流</p>
|
||||||
<div class="sidebar-card">
|
|
||||||
<span class="card-label">Account</span>
|
|
||||||
<strong>{{ currentUser?.phone }}</strong>
|
|
||||||
<span class="badge" :class="streamState">{{ streamState }}</span>
|
|
||||||
<div class="sidebar-actions">
|
|
||||||
<button class="reconnect" @click="connectStream">Reconnect</button>
|
|
||||||
<button class="reconnect" @click="clearSession">Logout</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="main-head-actions">
|
||||||
<div class="config-panel">
|
<span class="badge" :class="streamState">{{ streamState }}</span>
|
||||||
<label>
|
<button class="reconnect" @click="connectStream">重连</button>
|
||||||
API
|
<button class="reconnect" @click="clearSession">退出</button>
|
||||||
<input v-model="apiBase" placeholder="http://127.0.0.1:9000" />
|
|
||||||
</label>
|
|
||||||
<label v-if="!localAuthOnly">
|
|
||||||
AUTH API
|
|
||||||
<input v-model="authBase" placeholder="http://127.0.0.1:9100" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
sender
|
|
||||||
<input v-model="senderId" placeholder="web-user" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
chat
|
|
||||||
<input v-model="chatId" placeholder="web-user" />
|
|
||||||
</label>
|
|
||||||
<label v-if="!localAuthOnly">
|
|
||||||
token
|
|
||||||
<input v-model="apiToken" placeholder="Bearer token" readonly />
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</header>
|
||||||
|
|
||||||
<section class="workspace-main">
|
<div class="messages" ref="messagesEl">
|
||||||
<header class="main-head">
|
<article
|
||||||
<h1>智能对话</h1>
|
v-for="msg in messages"
|
||||||
<p>和 nanobot 的实时会话</p>
|
:key="msg.id"
|
||||||
</header>
|
class="bubble"
|
||||||
|
:class="[msg.role, msg.kind]"
|
||||||
|
>
|
||||||
|
<p>{{ msg.content }}</p>
|
||||||
|
<small>{{ msg.at }}</small>
|
||||||
|
</article>
|
||||||
|
<article v-if="assistantThinking" class="bubble assistant thinking">
|
||||||
|
<p>
|
||||||
|
<span class="thinking-spinner" aria-hidden="true"></span>
|
||||||
|
智能助手正在思考...
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="messages" ref="messagesEl">
|
<footer class="composer">
|
||||||
<article
|
<textarea
|
||||||
v-for="msg in messages"
|
v-model="text"
|
||||||
:key="msg.id"
|
rows="2"
|
||||||
class="bubble"
|
placeholder="给 nanobot 发送消息"
|
||||||
:class="[msg.role, msg.kind]"
|
@keydown.enter.exact.prevent="sendMessage"
|
||||||
>
|
/>
|
||||||
<p>{{ msg.content }}</p>
|
<button :disabled="!canSend" @click="sendMessage">发送</button>
|
||||||
<small>{{ msg.role }} · {{ msg.at }}</small>
|
</footer>
|
||||||
</article>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="composer">
|
|
||||||
<textarea
|
|
||||||
v-model="text"
|
|
||||||
rows="2"
|
|
||||||
placeholder="给 nanobot 发送消息"
|
|
||||||
@keydown.enter.exact.prevent="sendMessage"
|
|
||||||
/>
|
|
||||||
<button :disabled="!canSend" @click="sendMessage">发送</button>
|
|
||||||
</footer>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
21
src/assets/baidu-cloud-mark.svg
Normal file
21
src/assets/baidu-cloud-mark.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="240" height="180" viewBox="0 0 240 180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Baidu Cloud style mark">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="b1" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#31C8E8"/>
|
||||||
|
<stop offset="100%" stop-color="#1B96DE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="b2" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#FF6B8A"/>
|
||||||
|
<stop offset="100%" stop-color="#F04A79"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<circle cx="120" cy="60" r="36" fill="none" stroke="url(#b1)" stroke-width="16"/>
|
||||||
|
<circle cx="73" cy="92" r="30" fill="none" stroke="url(#b1)" stroke-width="16"/>
|
||||||
|
<path d="M 130 96 C 165 96 175 135 151 148" fill="none" stroke="url(#b1)" stroke-width="16" stroke-linecap="round"/>
|
||||||
|
<path d="M 93 80 C 98 90 110 95 120 95 C 130 95 142 90 147 80" fill="none" stroke="url(#b2)" stroke-width="14" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<text x="120" y="168" text-anchor="middle" font-size="26" font-family="'Noto Sans SC', sans-serif" fill="#2C67D9" font-weight="700">
|
||||||
|
百度 AI+
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/baidu2.png
Normal file
BIN
src/assets/baidu2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
124
src/style.css
124
src/style.css
@@ -113,14 +113,38 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
background: rgba(255, 255, 255, 0.58);
|
background: rgba(255, 255, 255, 0.58);
|
||||||
|
min-height: calc(100vh - 2.4rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-head {
|
.main-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.9rem;
|
||||||
padding: 1rem 1.2rem 0.7rem;
|
padding: 1rem 1.2rem 0.7rem;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: rgba(255, 255, 255, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-head-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-logo {
|
||||||
|
width: 168px;
|
||||||
|
height: 44px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-head-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
.main-head h1 {
|
.main-head h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.08rem;
|
font-size: 1.08rem;
|
||||||
@@ -148,6 +172,69 @@ body {
|
|||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-headline {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.85rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-logo {
|
||||||
|
width: 82px;
|
||||||
|
height: 64px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-headline h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.06rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-headline p {
|
||||||
|
margin: 0.22rem 0 0;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tag {
|
||||||
|
border: 1px solid #c8d9fb;
|
||||||
|
background: #f1f6ff;
|
||||||
|
color: #2f67dc;
|
||||||
|
padding: 0.22rem 0.52rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-code-btn {
|
||||||
|
min-width: 84px;
|
||||||
|
background: #f3f7ff;
|
||||||
|
color: var(--brand);
|
||||||
|
border: 1px solid #c7d8fb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-code-btn:disabled {
|
||||||
|
background: #eef1f7;
|
||||||
|
color: #93a2bb;
|
||||||
|
border-color: #d6deea;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-actions {
|
.auth-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
@@ -280,6 +367,32 @@ textarea:focus {
|
|||||||
background: #fff3f3;
|
background: #fff3f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bubble.thinking {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: #f7faff;
|
||||||
|
border-color: #c8d8f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-spinner {
|
||||||
|
width: 0.95rem;
|
||||||
|
height: 0.95rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid #bed0f3;
|
||||||
|
border-top-color: #2b63d9;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.45rem;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
vertical-align: -0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bubble p {
|
.bubble p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@@ -349,6 +462,17 @@ button:disabled {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-head {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-head-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user