实现改写文案标题
This commit is contained in:
parent
ccdbbabb1e
commit
f9b031e8da
@ -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
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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']
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: () => []
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user