feat: add brand config to workspace and Blade prompt system for AI
This commit is contained in:
parent
baf310112e
commit
5493871b2d
10 changed files with 162 additions and 4 deletions
|
|
@ -125,7 +125,7 @@ public function store(
|
|||
->map(fn (AiMessage $m) => ['role' => $m->role, 'content' => $m->content])
|
||||
->all();
|
||||
|
||||
$responseContent = $textService->generate($prompt, $history);
|
||||
$responseContent = $textService->generate($prompt, $history, $workspace);
|
||||
}
|
||||
|
||||
$assistantMessage = $post->aiMessages()->create([
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ public function rules(): array
|
|||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'timezone' => ['required', 'string', new Timezone],
|
||||
'brand_website' => ['nullable', 'url', 'max:255'],
|
||||
'brand_description' => ['nullable', 'string', 'max:2000'],
|
||||
'brand_tone' => ['nullable', 'string', 'in:professional,casual,friendly,bold,inspirational,humorous,educational'],
|
||||
'brand_voice_notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ class Workspace extends Model
|
|||
'user_id',
|
||||
'name',
|
||||
'timezone',
|
||||
'brand_website',
|
||||
'brand_description',
|
||||
'brand_tone',
|
||||
'brand_voice_notes',
|
||||
];
|
||||
|
||||
protected $appends = ['has_logo', 'logo_url'];
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
|
|
@ -21,12 +22,20 @@ public function __construct()
|
|||
/**
|
||||
* @param array<int, array{role: string, content: string}> $history
|
||||
*/
|
||||
public function generate(string $prompt, array $history = []): string
|
||||
public function generate(string $prompt, array $history = [], ?Workspace $workspace = null): string
|
||||
{
|
||||
$systemPrompt = view('prompts.assistant.system', [
|
||||
'brandName' => $workspace?->name ?? '',
|
||||
'brandDescription' => $workspace?->brand_description ?? '',
|
||||
'brandWebsite' => $workspace?->brand_website ?? '',
|
||||
'tone' => $workspace?->brand_tone ?? 'professional',
|
||||
'voiceNotes' => $workspace?->brand_voice_notes ?? '',
|
||||
])->render();
|
||||
|
||||
$messages = [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => 'You are a social media content expert. Help users write engaging captions, hashtags, and post content. Be creative, concise, and on-brand. Respond in the same language the user writes in.',
|
||||
'content' => $systemPrompt,
|
||||
],
|
||||
...$history,
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ public function up(): void
|
|||
$table->uuid('user_id')->nullable();
|
||||
$table->string('name');
|
||||
$table->string('timezone');
|
||||
$table->string('brand_website')->nullable();
|
||||
$table->text('brand_description')->nullable();
|
||||
$table->string('brand_tone')->default('professional');
|
||||
$table->text('brand_voice_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
|
||||
|
|
|
|||
|
|
@ -84,6 +84,25 @@
|
|||
'save' => 'Save',
|
||||
],
|
||||
|
||||
'brand' => [
|
||||
'title' => 'Brand',
|
||||
'description' => 'Configure your brand identity for AI-generated content.',
|
||||
'website' => 'Website',
|
||||
'website_placeholder' => 'https://yourbrand.com',
|
||||
'brand_description' => 'Description',
|
||||
'brand_description_placeholder' => 'Tell us about your brand, what you do, and who your audience is...',
|
||||
'tone' => 'Tone of voice',
|
||||
'tone_professional' => 'Professional',
|
||||
'tone_casual' => 'Casual',
|
||||
'tone_friendly' => 'Friendly',
|
||||
'tone_bold' => 'Bold',
|
||||
'tone_inspirational' => 'Inspirational',
|
||||
'tone_humorous' => 'Humorous',
|
||||
'tone_educational' => 'Educational',
|
||||
'voice_notes' => 'Voice notes',
|
||||
'voice_notes_placeholder' => 'Additional writing guidelines, words to avoid, style preferences...',
|
||||
],
|
||||
|
||||
'members' => [
|
||||
'title' => 'Members',
|
||||
'heading' => 'Team members',
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,13 @@ import {
|
|||
} from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -31,6 +38,7 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { destroy as destroyInvite } from '@/routes/app/invites';
|
||||
import { remove as removeMemberRoute, updateRole } from '@/routes/app/members';
|
||||
|
|
@ -41,6 +49,10 @@ interface Workspace {
|
|||
timezone: string;
|
||||
has_logo: boolean;
|
||||
logo_url: string | null;
|
||||
brand_website: string | null;
|
||||
brand_description: string | null;
|
||||
brand_tone: string;
|
||||
brand_voice_notes: string | null;
|
||||
}
|
||||
|
||||
interface Member {
|
||||
|
|
@ -64,6 +76,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const timezone = ref(props.workspace.timezone);
|
||||
const brandTone = ref(props.workspace.brand_tone ?? 'professional');
|
||||
const inviteDialogOpen = ref(false);
|
||||
|
||||
const removeMemberModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);
|
||||
|
|
@ -136,6 +149,82 @@ const changeRole = (member: Member, role: string) => {
|
|||
|
||||
<Separator />
|
||||
|
||||
<div class="flex flex-col space-y-6">
|
||||
<HeadingSmall
|
||||
:title="$t('settings.brand.title')"
|
||||
:description="$t('settings.brand.description')"
|
||||
/>
|
||||
|
||||
<Form
|
||||
v-bind="WorkspaceController.updateSettings.form()"
|
||||
class="space-y-6"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<input type="hidden" name="name" :value="workspace.name" />
|
||||
<input type="hidden" name="timezone" :value="workspace.timezone" />
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="brand_website">{{ $t('settings.brand.website') }}</Label>
|
||||
<Input
|
||||
id="brand_website"
|
||||
name="brand_website"
|
||||
type="url"
|
||||
:default-value="workspace.brand_website ?? ''"
|
||||
:placeholder="trans('settings.brand.website_placeholder')"
|
||||
/>
|
||||
<InputError :message="errors.brand_website" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="brand_description">{{ $t('settings.brand.brand_description') }}</Label>
|
||||
<Textarea
|
||||
id="brand_description"
|
||||
name="brand_description"
|
||||
:default-value="workspace.brand_description ?? ''"
|
||||
:placeholder="trans('settings.brand.brand_description_placeholder')"
|
||||
rows="3"
|
||||
/>
|
||||
<InputError :message="errors.brand_description" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="brand_tone">{{ $t('settings.brand.tone') }}</Label>
|
||||
<Select v-model="brandTone" name="brand_tone">
|
||||
<SelectTrigger id="brand_tone">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="professional">{{ $t('settings.brand.tone_professional') }}</SelectItem>
|
||||
<SelectItem value="casual">{{ $t('settings.brand.tone_casual') }}</SelectItem>
|
||||
<SelectItem value="friendly">{{ $t('settings.brand.tone_friendly') }}</SelectItem>
|
||||
<SelectItem value="bold">{{ $t('settings.brand.tone_bold') }}</SelectItem>
|
||||
<SelectItem value="inspirational">{{ $t('settings.brand.tone_inspirational') }}</SelectItem>
|
||||
<SelectItem value="humorous">{{ $t('settings.brand.tone_humorous') }}</SelectItem>
|
||||
<SelectItem value="educational">{{ $t('settings.brand.tone_educational') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="brand_tone" :value="brandTone" />
|
||||
<InputError :message="errors.brand_tone" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="brand_voice_notes">{{ $t('settings.brand.voice_notes') }}</Label>
|
||||
<Textarea
|
||||
id="brand_voice_notes"
|
||||
name="brand_voice_notes"
|
||||
:default-value="workspace.brand_voice_notes ?? ''"
|
||||
:placeholder="trans('settings.brand.voice_notes_placeholder')"
|
||||
rows="3"
|
||||
/>
|
||||
<InputError :message="errors.brand_voice_notes" />
|
||||
</div>
|
||||
|
||||
<Button :disabled="processing">{{ $t('settings.workspace.save') }}</Button>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="flex flex-col space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<HeadingSmall
|
||||
|
|
|
|||
9
resources/views/prompts/assistant/image.blade.php
Normal file
9
resources/views/prompts/assistant/image.blade.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Create a social media image.
|
||||
@if($brandName)
|
||||
Brand: {{ $brandName }}
|
||||
@endif
|
||||
@if($tone)
|
||||
Style should match a {{ $tone }} tone.
|
||||
@endif
|
||||
|
||||
User request: {{ $prompt }}
|
||||
20
resources/views/prompts/assistant/system.blade.php
Normal file
20
resources/views/prompts/assistant/system.blade.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
You are a social media content expert.
|
||||
@if($brandName)
|
||||
You are creating content for {{ $brandName }}.
|
||||
@endif
|
||||
@if($brandDescription)
|
||||
|
||||
About the brand: {{ $brandDescription }}
|
||||
@endif
|
||||
@if($brandWebsite)
|
||||
|
||||
Brand website: {{ $brandWebsite }}
|
||||
@endif
|
||||
|
||||
Tone of voice: {{ $tone }}
|
||||
@if($voiceNotes)
|
||||
|
||||
Additional guidelines: {{ $voiceNotes }}
|
||||
@endif
|
||||
|
||||
Help users write engaging captions, hashtags, and post content. Be creative, concise, and on-brand. Respond in the same language the user writes in.
|
||||
Loading…
Reference in a new issue