实现改写文案标题

This commit is contained in:
mcallzbl 2025-04-02 19:28:39 +08:00
parent ccdbbabb1e
commit f9b031e8da
5 changed files with 195 additions and 46 deletions

View File

@ -114,6 +114,58 @@ export default class IPCs {
}
})
ipcMain.handle(
'call-openai-title',
async (event, baseUrl: string, apiKey: string, model: string, count: number, text: string) => {
try {
const client = new OpenAI({
baseURL: baseUrl,
apiKey: apiKey,
maxRetries: 2,
timeout: 30000
})
// 创建 count 个请求
const requests = Array.from({ length: count }, (_, index) => {
return client.chat.completions
.create({
model: model,
messages: [
{
role: 'system',
content: '你是一个小红书标题写手,能够熟练地根据用户的输入,改写成内容相近,但表达方式不同的新标题。你的标题中需要具备吸人眼球的钩子,能够牢牢抓住用户的注意力。请直接输出新的标题,不要输出其他任何提示性词语, 以纯文本的形式输出'
},
{ role: 'user', content: text }
],
max_tokens: 100,
temperature: 0.8,
stream: true
})
.then(async (stream) => {
let fullResponse = ''
for await (const chunk of stream) {
const content = chunk.choices[0].delta.content || ''
fullResponse += content
// 实时发送每个请求的部分结果
event.sender.send('openai-partial-response', { index, content })
}
// 返回最终结果
return { index, response: fullResponse.trim() }
})
})
// 并发执行所有请求
const results = await Promise.all(requests)
// 返回所有请求的最终结果
return results
} catch (error) {
console.error('生成标题失败:', error)
throw error
}
}
)
ipcMain.handle('get-api-key', () => {
return store.get('api-key') || null
})
@ -145,5 +197,7 @@ export default class IPCs {
})
return dialogResult
})
}
}

View File

@ -12,7 +12,8 @@ const mainAvailChannels: string[] = [
'call-openai',
'openai-partial-response',
'get-api-key',
'set-api-key'
'set-api-key',
'call-openai-title'
]
const rendererAvailChannels: string[] = ['openai-partial-response', 'get-api-key', 'set-api-key']

View File

@ -66,10 +66,24 @@
></textarea>
</div>
<button :disabled="isGenerating" @click="handleRewrite" class="rewrite-button">
<span v-if="isGenerating" class="loading-spinner"></span>
<span v-else>{{ mode === 'single' ? '生成改写' : '批量生成' }}</span>
</button>
<div class="button-group">
<button
:disabled="isGenerating || isGeneratingTitle"
@click="handleRewrite('content')"
class="rewrite-button"
>
<span v-if="isGenerating" class="loading-spinner"></span>
<span v-else>改写文案</span>
</button>
<button
:disabled="isGenerating || isGeneratingTitle"
@click="handleRewrite('title')"
class="rewrite-button title-button"
>
<span v-if="isGeneratingTitle" class="loading-spinner"></span>
<span v-else>改写标题</span>
</button>
</div>
</div>
</template>
@ -79,11 +93,14 @@ import { ARTICLE_MAX_COUNT } from '../constants/constants'
import HeaderComponent from './HeaderComponent.vue'
import SelectModel from './SelectModelComponent.vue'
// 使 isGenerating
const { isGenerating } = defineProps({
const { isGenerating, isGeneratingTitle } = defineProps({
isGenerating: {
type: Boolean,
default: false
},
isGeneratingTitle: {
type: Boolean,
default: false
}
})
@ -108,11 +125,16 @@ watch(currentModel, (newValue) => {
})
const validateInput = () => {
if (!originalText.value.trim()) {
return false
}
if (generationCount.value < 1) {
setGenerationCount(1)
} else if (generationCount.value > ARTICLE_MAX_COUNT) {
setGenerationCount(ARTICLE_MAX_COUNT)
}
return true
}
const setGenerationCount = async (value) => {
@ -138,20 +160,38 @@ const decrementCount = () => {
}
}
const handleRewrite = () => {
if (!originalText.value.trim()) {
// emit
const handleRewrite = async (type) => {
if (!validateInput()) {
showToast('请输入需要改写的文本')
return
}
emit('rewrite', {
mode: mode.value,
text: originalText.value,
count: mode.value === 'single' ? 1 : generationCount.value,
model: currentModel.value,
key: currentKey.value
key: currentKey.value,
type
})
}
const showToast = (message) => {
const toast = document.createElement('div')
toast.className = 'toast-message'
toast.textContent = message
document.body.appendChild(toast)
setTimeout(() => {
toast.classList.add('show')
setTimeout(() => {
toast.classList.remove('show')
setTimeout(() => {
document.body.removeChild(toast)
}, 300)
}, 2000)
}, 10)
}
</script>
<style scoped>
@ -329,7 +369,14 @@ const handleRewrite = () => {
background-color: white;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 16px;
}
.rewrite-button {
flex: 1;
background: linear-gradient(135deg, #2ecc71, #27ae60);
border: none;
border-radius: 10px;
@ -337,10 +384,8 @@ const handleRewrite = () => {
cursor: pointer;
font-size: 16px;
font-weight: 600;
margin-top: 16px;
padding: 16px;
transition: all 0.3s ease;
width: 100%;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
@ -377,6 +422,19 @@ const handleRewrite = () => {
}
}
.title-button {
background: linear-gradient(135deg, #3498db, #2980b9);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}
.title-button:hover:not(:disabled) {
box-shadow: 0 6px 16px rgba(52, 152, 219, 0.4);
}
.title-button:active:not(:disabled) {
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
}
@media (max-width: 1200px) {
.input-section {
flex: none;

View File

@ -1,6 +1,6 @@
<template>
<transition name="fade">
<div v-if="results.length > 0 || isGenerating" class="result-section">
<div v-if="(results?.length > 0) || isGenerating" class="result-section">
<div class="result-header-bar">
<h2 class="section-title">
<span class="section-icon"></span>
@ -24,7 +24,7 @@
</button>
</div>
<span class="model-badge">{{ model }}</span>
<span class="count-badge">{{ results.length }} 个结果</span>
<span class="count-badge">{{ results?.length || 0 }} 个结果</span>
</div>
</div>
@ -84,7 +84,7 @@
<script setup>
import { ref, reactive } from 'vue'
const { results, isGenerating, model, layoutMode } = defineProps({
const { results = [], isGenerating, model, layoutMode } = defineProps({
results: {
type: Array,
default: () => []

View File

@ -4,12 +4,13 @@
<DisclaimerComponent />
<InputSection
:is-generating="isGenerating"
:is-generating-title="isGeneratingTitle"
v-model:model="currentModel"
@rewrite="handleRewrite"
/>
<ResultSection
:results="rewrittenText"
:is-generating="isGenerating"
:is-generating="isGenerating || isGeneratingTitle"
:model="currentModel"
:layout-mode="layoutMode"
@update:layout="(newLayout) => layoutMode = newLayout"
@ -29,6 +30,7 @@ import FooterComponent from '../components/FooterComponent.vue'
const rewrittenText = ref([])
const isGenerating = ref(false)
const isGeneratingTitle = ref(false)
const layoutMode = ref('list')
const currentModel = ref('gpt-4o')
@ -60,37 +62,71 @@ const showToast = (message) => {
}, 10)
}
const handleRewrite = async ({ mode, text, count, model, key }) => {
isGenerating.value = true
rewrittenText.value = []
const handleRewrite = async ({ mode, text, count, model, key, type }) => {
if (type === 'title') {
isGeneratingTitle.value = true
rewrittenText.value = []
try {
const baseUrl = await window.mainApi.invoke('getBaseUrl')
window.mainApi.on('openai-partial-response', (_, { index, content }) => {
if (!rewrittenText.value[index]) {
rewrittenText.value[index] = ''
}
rewrittenText.value[index] += content
})
try {
const baseUrl = await window.mainApi.invoke('getBaseUrl')
window.mainApi.on('openai-partial-response', (_, { index, content }) => {
if (!rewrittenText.value[index]) {
rewrittenText.value[index] = ''
}
rewrittenText.value[index] += content
})
const results = await window.mainApi.invoke(
'call-openai',
baseUrl,
key,
model,
count,
text
)
const results = await window.mainApi.invoke(
'call-openai-title',
baseUrl,
key,
model,
count,
text
)
results.forEach(({ index, response }) => {
rewrittenText.value[index] = response
})
} catch (error) {
console.error('调用 API 失败:', error)
showToast('调用 API 失败,请稍后重试')
} finally {
isGenerating.value = false
results.forEach(({ index, response }) => {
rewrittenText.value[index] = response
})
} catch (error) {
console.error('生成标题失败:', error)
showToast('生成标题失败,请稍后重试')
} finally {
isGeneratingTitle.value = false
}
} else {
isGenerating.value = true
rewrittenText.value = []
try {
const baseUrl = await window.mainApi.invoke('getBaseUrl')
window.mainApi.on('openai-partial-response', (_, { index, content }) => {
if (!rewrittenText.value[index]) {
rewrittenText.value[index] = ''
}
rewrittenText.value[index] += content
})
const results = await window.mainApi.invoke(
'call-openai',
baseUrl,
key,
model,
count,
text
)
results.forEach(({ index, response }) => {
rewrittenText.value[index] = response
})
} catch (error) {
console.error('调用 API 失败:', error)
showToast('调用 API 失败,请稍后重试')
} finally {
isGenerating.value = false
}
}
}