AI Blog Writer - Implement modular architecture with research, outline, and core services
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useCopilotAction } from '@copilotkit/react-core';
|
||||
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi';
|
||||
|
||||
@@ -9,23 +9,136 @@ interface KeywordInputFormProps {
|
||||
onResearchComplete?: (researchData: BlogResearchResponse) => void;
|
||||
}
|
||||
|
||||
export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsReceived, onResearchComplete }) => {
|
||||
// State for button enable/disable only
|
||||
const [hasInput, setHasInput] = useState(false);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const selectRef = useRef<HTMLSelectElement>(null);
|
||||
// Separate component to manage form state
|
||||
const ResearchForm: React.FC<{
|
||||
prompt?: string;
|
||||
onSubmit: (data: { keywords: string; blogLength: string }) => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ prompt, onSubmit, onCancel }) => {
|
||||
const [keywords, setKeywords] = useState('');
|
||||
const [blogLength, setBlogLength] = useState('1000');
|
||||
const hasValidInput = keywords.trim().length > 0;
|
||||
|
||||
// Focus input when form appears
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 100);
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (hasValidInput) {
|
||||
onSubmit({ keywords: keywords.trim(), blogLength });
|
||||
} else {
|
||||
window.alert('Please enter keywords or a topic to start research.');
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #e0e0e0',
|
||||
margin: '8px 0'
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
|
||||
🔍 Let's Research Your Blog Topic
|
||||
</h4>
|
||||
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
|
||||
{prompt || 'Please provide the keywords or topic you want to research for your blog:'}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
|
||||
Keywords or Topic *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={keywords}
|
||||
onChange={(e) => setKeywords(e.target.value)}
|
||||
onFocus={(e) => e.target.select()}
|
||||
placeholder="e.g., artificial intelligence, machine learning, AI trends"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '2px solid #1976d2',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none',
|
||||
backgroundColor: 'white',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
|
||||
Blog Length (words)
|
||||
</label>
|
||||
<select
|
||||
value={blogLength}
|
||||
onChange={(e) => setBlogLength(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '2px solid #1976d2',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none',
|
||||
backgroundColor: 'white',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
<option value="500">500 words (Short blog)</option>
|
||||
<option value="1000">1000 words (Medium blog)</option>
|
||||
<option value="1500">1500 words (Long blog)</option>
|
||||
<option value="2000">2000+ words (Comprehensive guide)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!hasValidInput}
|
||||
style={{
|
||||
backgroundColor: hasValidInput ? '#1976d2' : '#ccc',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '10px 20px',
|
||||
cursor: hasValidInput ? 'pointer' : 'not-allowed',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
🚀 Start Research {hasValidInput ? '(Enabled)' : '(Disabled)'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '6px',
|
||||
padding: '10px 20px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
color: '#666'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsReceived, onResearchComplete }) => {
|
||||
|
||||
// Keyword input action with Human-in-the-Loop
|
||||
useCopilotActionTyped({
|
||||
@@ -51,143 +164,14 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
key="keyword-input-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
<ResearchForm
|
||||
prompt={args.prompt}
|
||||
onSubmit={(formData) => {
|
||||
onKeywordsReceived?.(formData);
|
||||
respond?.(JSON.stringify(formData));
|
||||
}}
|
||||
style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #e0e0e0',
|
||||
margin: '8px 0'
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
|
||||
🔍 Let's Research Your Blog Topic
|
||||
</h4>
|
||||
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
|
||||
{args.prompt || 'Please provide the keywords or topic you want to research for your blog:'}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
|
||||
Keywords or Topic *
|
||||
</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
defaultValue=""
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Update state for button enable/disable
|
||||
setHasInput(value.trim().length > 0);
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
placeholder="e.g., artificial intelligence, machine learning, AI trends"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '2px solid #1976d2',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none',
|
||||
backgroundColor: 'white',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
|
||||
Blog Length (words)
|
||||
</label>
|
||||
<select
|
||||
ref={selectRef}
|
||||
defaultValue="1000"
|
||||
onChange={(e) => {
|
||||
// No state update needed for select
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '2px solid #1976d2',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none',
|
||||
backgroundColor: 'white',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
<option value="500">500 words (Short blog)</option>
|
||||
<option value="1000">1000 words (Medium blog)</option>
|
||||
<option value="1500">1500 words (Long blog)</option>
|
||||
<option value="2000">2000+ words (Comprehensive guide)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const kw = (inputRef.current?.value || '').trim();
|
||||
const len = (selectRef.current?.value || '1000');
|
||||
if (kw) {
|
||||
const formData = {
|
||||
keywords: kw,
|
||||
blogLength: len
|
||||
};
|
||||
|
||||
// Notify parent component if callback provided
|
||||
onKeywordsReceived?.(formData);
|
||||
|
||||
// Send to CopilotKit to trigger performResearch action
|
||||
respond?.(JSON.stringify(formData));
|
||||
}
|
||||
}}
|
||||
disabled={!hasInput}
|
||||
style={{
|
||||
backgroundColor: hasInput ? '#1976d2' : '#f5f5f5',
|
||||
color: hasInput ? 'white' : '#999',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '10px 20px',
|
||||
cursor: hasInput ? 'pointer' : 'not-allowed',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
🚀 Start Research
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
respond?.('CANCEL');
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '6px',
|
||||
padding: '10px 20px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
color: '#666'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
onCancel={() => respond?.('CANCEL')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -204,7 +188,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
|
||||
const data = JSON.parse(formData);
|
||||
const { keywords, blogLength } = data;
|
||||
|
||||
// If keywords is a topic description, extract keywords from it
|
||||
const keywordList = keywords.includes(',')
|
||||
? keywords.split(',').map((k: string) => k.trim())
|
||||
: keywords.split(' ').filter((k: string) => k.length > 2).slice(0, 5);
|
||||
@@ -217,8 +200,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
|
||||
};
|
||||
|
||||
const res = await blogWriterApi.research(payload);
|
||||
|
||||
// Notify parent component
|
||||
onResearchComplete?.(res);
|
||||
|
||||
const sourcesCount = res.sources?.length || 0;
|
||||
@@ -246,7 +227,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
|
||||
}
|
||||
},
|
||||
render: ({ status }: any) => {
|
||||
console.log('performResearch render called with status:', status);
|
||||
if (status === 'inProgress' || status === 'executing') {
|
||||
return (
|
||||
<div style={{
|
||||
|
||||
Reference in New Issue
Block a user