Animate model name in code example

This commit is contained in:
Christopher Hertel
2025-11-18 22:50:11 +01:00
parent 26299d3e1a
commit 6ef213167b
11 changed files with 180 additions and 2 deletions

View File

@@ -1,3 +1,4 @@
import './stimulus_bootstrap.js';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles/app.css';

View File

@@ -0,0 +1,4 @@
{
"controllers": [],
"entrypoints": []
}

View File

@@ -0,0 +1,81 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
// and thus this event-listener will not be executed.
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener('turbo:submit-start', function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener('turbo:submit-end', function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});
export function generateCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
let csrfToken = csrfField.value;
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
export function generateCsrfHeaders (formElement) {
const headers = {};
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return headers;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}
return headers;
}
export function removeCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
/* stimulusFetch: 'lazy' */
export default 'csrf-protection-controller';

View File

@@ -0,0 +1,56 @@
import { Controller } from '@hotwired/stimulus';
import Typed from 'typed.js';
export default class extends Controller {
static values = {
strings: Array,
typeSpeed: { type: Number, default: 30 },
smartBackspace: { type: Boolean, default: true },
startDelay: Number,
backSpeed: Number,
shuffle: Boolean,
backDelay: { type: Number, default: 700 },
fadeOut: Boolean,
fadeOutClass: { type: String, default: 'typed-fade-out' },
fadeOutDelay: { type: Number, default: 500 },
loop: Boolean,
loopCount: { type: Number, default: Number.POSITIVE_INFINITY },
showCursor: { type: Boolean, default: true },
cursorChar: { type: String, default: '.' },
autoInsertCss: { type: Boolean, default: true },
attr: String,
bindInputFocusEvents: Boolean,
contentType: { type: String, default: 'html' },
};
connect() {
const options = {
strings: this.stringsValue,
typeSpeed: this.typeSpeedValue,
smartBackspace: this.smartBackspaceValue,
startDelay: this.startDelayValue,
backSpeed: this.backSpeedValue,
shuffle: this.shuffleValue,
backDelay: this.backDelayValue,
fadeOut: this.fadeOutValue,
fadeOutClass: this.fadeOutClassValue,
fadeOutDelay: this.fadeOutDelayValue,
loop: this.loopValue,
loopCount: this.loopCountValue,
showCursor: this.showCursorValue,
cursorChar: this.cursorCharValue,
autoInsertCss: this.autoInsertCssValue,
attr: this.attrValue,
bindInputFocusEvents: this.bindInputFocusEventsValue,
contentType: this.contentTypeValue,
};
this.dispatchEvent('pre-connect', { options });
const typed = new Typed(this.element, options);
this.dispatchEvent('connect', { typed, options });
}
dispatchEvent(name, payload) {
this.dispatch(name, { detail: payload, prefix: 'typed' });
}
}

View File

@@ -0,0 +1,5 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

View File

@@ -14,6 +14,7 @@
"symfony/flex": "^2",
"symfony/framework-bundle": "*",
"symfony/runtime": "*",
"symfony/stimulus-bundle": "^2.31",
"symfony/twig-bundle": "*",
"symfony/ux-icons": "^2.31",
"symfony/yaml": "*",

View File

@@ -6,4 +6,5 @@ return [
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
];

View File

@@ -26,4 +26,13 @@ return [
'version' => '5.3.8',
'type' => 'css',
],
'typed.js' => [
'version' => '2.1.0',
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
];

View File

@@ -81,6 +81,21 @@
"config/routes.yaml"
]
},
"symfony/stimulus-bundle": {
"version": "2.31",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.24",
"ref": "3357f2fa6627b93658d8e13baa416b2a94a50c5f"
},
"files": [
"assets/controllers.json",
"assets/controllers/csrf_protection_controller.js",
"assets/controllers/typed_controller.js",
"assets/stimulus_bootstrap.js"
]
},
"symfony/twig-bundle": {
"version": "7.3",
"recipe": {

View File

@@ -2,7 +2,7 @@
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container py-3">
<a class="navbar-brand" href="{{ path('homepage') }}">
{{ ux_icon('logos:symfony-ai', {width: 'auto', height: 64}) }}
{{ ux_icon('logos:symfony-ai', {width: 225, height: 64}) }}
</a>
<button

View File

@@ -29,8 +29,13 @@
</div>
<div class="col-xl-6 mt-5 mt-xl-0">
{% set model_typed = stimulus_controller('typed', {
loop: true,
cursorChar: '|',
strings: ['gpt-4o', 'mistral-large-latest', 'gemini-2.0-flash', 'claude-sonnet-4-0'],
}) %}
<pre class="terminal"><code><span class="variable">$platform</span> = <span class="title class_">PlatformFactory</span>::<span class="title function_ invoke__">create</span>(...);
<span class="variable">$agent</span> = <span class="keyword">new</span> <span class="title class_">Agent</span>(<span class="variable">$platform</span>, <span class="string">'gpt-4o'</span>)
<span class="variable">$agent</span> = <span class="keyword">new</span> <span class="title class_">Agent</span>(<span class="variable">$platform</span>, <span class="string">'</span><span class="string" {{ model_typed|raw }}></span><span class="string">'</span>)
<span class="variable">$messages</span> = <span class="keyword">new</span> <span class="title class_">MessageBag</span>(
<span class="title class_">Message</span>::<span class="title function_ invoke__">forSystem</span>(<span class="string">'You are a pirate and you write funny.'</span>),