diff --git a/.env.ci b/.env.ci index 8ca79ca..1573ca2 100644 --- a/.env.ci +++ b/.env.ci @@ -14,12 +14,12 @@ LOG_LEVEL=debug FLARE_KEY= -DB_CONNECTION=mysql +DB_CONNECTION=pgsql DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=laravel -DB_USERNAME=root -DB_PASSWORD=root +DB_PORT=5432 +DB_DATABASE=testing +DB_USERNAME=postgres +DB_PASSWORD=postgres FPH_ENABLED=true CSP_ENABLED=true diff --git a/.env.example b/.env.example index 39f79b7..4f25c82 100644 --- a/.env.example +++ b/.env.example @@ -57,4 +57,6 @@ CLOUDINARY_CLOUD_NAME=my-cloud-name CLOUDINARY_API_KEY=my-api-key CLOUDINARY_API_SECRET=my-api-secret -LARAVEL_DEFAULT_FATHOM_SITE_ID=WECFOADW \ No newline at end of file +LARAVEL_DEFAULT_FATHOM_SITE_ID=WECFOADW +LITELLM_URL=https://llm.codebar.net +LITELLM_MASTER_KEY= diff --git a/.github/workflows/pest_coverage_pull_request.yml b/.github/workflows/pest_coverage_pull_request.yml index 0e9df18..5924183 100644 --- a/.github/workflows/pest_coverage_pull_request.yml +++ b/.github/workflows/pest_coverage_pull_request.yml @@ -21,7 +21,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ env.PHP_VERSION }} - extensions: dom, curl, fileinfo, mysql, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, xdebug + extensions: dom, curl, fileinfo, pgsql, pdo_pgsql, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, xdebug coverage: xdebug - name: Prepare the .env file @@ -45,9 +45,9 @@ jobs: - name: Setup Database run: | - sudo systemctl start mysql - mysql --user="root" --password="root" -e "CREATE DATABASE laravel character set UTF8mb4 collate utf8mb4_bin;" - mysql --user="root" --password="root" -e "CREATE DATABASE logs character set UTF8mb4 collate utf8mb4_bin;" + sudo systemctl start postgresql.service + sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" + sudo -u postgres createdb testing - name: Generate App Key & Run Migrations run: | diff --git a/.github/workflows/pest_pull_request.yml b/.github/workflows/pest_pull_request.yml index e8891f9..1985284 100644 --- a/.github/workflows/pest_pull_request.yml +++ b/.github/workflows/pest_pull_request.yml @@ -19,7 +19,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ env.PHP_VERSION }} - extensions: dom, curl, fileinfo, mysql, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: dom, curl, fileinfo, pgsql, pdo_pgsql, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick - name: Prepare the .env file run: cp .env.ci .env @@ -42,8 +42,9 @@ jobs: - name: Setup Database run: | - sudo systemctl start mysql - mysql --user="root" --password="root" -e "CREATE DATABASE laravel character set UTF8mb4 collate utf8mb4_bin;" + sudo systemctl start postgresql.service + sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" + sudo -u postgres createdb testing - name: Generate App Key & Run Migrations run: | diff --git a/app/Actions/FetchLlmUsageAction.php b/app/Actions/FetchLlmUsageAction.php new file mode 100644 index 0000000..fc46575 --- /dev/null +++ b/app/Actions/FetchLlmUsageAction.php @@ -0,0 +1,60 @@ + + */ + public function fetchDay(CarbonImmutable $date): Collection + { + $response = $this->client()->get('/spend/logs', [ + 'start_date' => $date->toDateString(), + 'end_date' => $date->addDay()->toDateString(), + 'summarize' => 'false', + ])->throw(); + + return $this->parseSpendLogs($date, $response->json() ?? []); + } + + private function client(): PendingRequest + { + return Http::baseUrl(config('services.litellm.url')) + ->withToken(config('services.litellm.master_key')) + ->acceptJson() + ->connectTimeout(10) + ->timeout(120) + ->retry(times: 2, sleepMilliseconds: 1000, throw: false); + } + + /** + * @return Collection + */ + private function parseSpendLogs(CarbonImmutable $date, array $logs): Collection + { + // The end_date bound is inclusive, so the response can contain rows of + // the following day — keep only rows that started on the requested day. + return collect($logs) + ->filter(fn (mixed $log): bool => filled(data_get($log, 'model_group'))) + ->filter(fn (mixed $log): bool => str_starts_with((string) data_get($log, 'startTime'), $date->toDateString())) + ->groupBy(fn (mixed $log): string => data_get($log, 'model_group')) + ->map(fn (Collection $rows, string $model): array => [ + 'date' => $date->toDateString(), + 'model' => $model, + 'prompt_tokens' => (int) $rows->sum(fn (mixed $log): int => (int) data_get($log, 'prompt_tokens', 0)), + 'completion_tokens' => (int) $rows->sum(fn (mixed $log): int => (int) data_get($log, 'completion_tokens', 0)), + 'total_tokens' => (int) $rows->sum(fn (mixed $log): int => (int) data_get($log, 'total_tokens', 0)), + 'requests' => $rows->count(), + 'spend' => round($rows->sum(fn (mixed $log): float => (float) data_get($log, 'spend', 0)), 6), + ]) + ->values(); + } +} diff --git a/app/Actions/LlmUsageStatsAction.php b/app/Actions/LlmUsageStatsAction.php new file mode 100644 index 0000000..eaa8a71 --- /dev/null +++ b/app/Actions/LlmUsageStatsAction.php @@ -0,0 +1,134 @@ + + */ + public function models(): Collection + { + return $this->remember('models', function () { + return AiModelDailyUsage::query() + ->whereNotNull('ai_model_id') + ->distinct() + ->orderBy('model') + ->pluck('model'); + }); + } + + public function hasOtherModels(): bool + { + return $this->remember('has_other_models', function () { + return AiModelDailyUsage::query()->whereNull('ai_model_id')->exists(); + }); + } + + /** + * @return Collection + */ + public function years(): Collection + { + return $this->remember('years', function () { + return AiModelDailyUsage::query() + ->orderBy('date') + ->pluck('date') + ->map(fn (Carbon $date) => $date->format('Y')) + ->unique() + ->values(); + }); + } + + /** + * @return Collection + */ + public function monthlyBreakdown(?string $year, ?string $month, ?string $model): Collection + { + $suffix = 'breakdown_'.($year ?? 'all').'_'.($month ?? 'all').'_'.($model ?? 'all'); + + return $this->remember($suffix, function () use ($year, $month, $model) { + return AiModelDailyUsage::query() + ->when($model === self::OTHER_MODEL, fn (Builder $query) => $query->whereNull('ai_model_id')) + ->when($model && $model !== self::OTHER_MODEL, fn (Builder $query) => $query->where('model', $model)) + ->when($year, fn (Builder $query) => $query->whereYear('date', $year)) + ->when($month, fn (Builder $query) => $query->whereMonth('date', $month)) + ->orderBy('date') + ->get() + ->groupBy(fn (AiModelDailyUsage $row) => $row->date->format('Y-m')) + ->map(fn (Collection $rows, string $label) => [ + 'label' => $label, + 'prompt_tokens' => (int) $rows->sum('prompt_tokens'), + 'completion_tokens' => (int) $rows->sum('completion_tokens'), + 'total_tokens' => (int) $rows->sum('total_tokens'), + 'requests' => (int) $rows->sum('requests'), + ]) + ->values(); + }); + } + + /** + * @return array{prompt_tokens: int, completion_tokens: int, total_tokens: int, requests: int} + */ + public function currentMonthSummary(?string $model = null): array + { + return $this->summary('month_summary', CarbonImmutable::now()->startOfMonth(), $model); + } + + /** + * @return array{prompt_tokens: int, completion_tokens: int, total_tokens: int, requests: int} + */ + public function currentYearSummary(?string $model = null): array + { + return $this->summary('year_summary', CarbonImmutable::now()->startOfYear(), $model); + } + + /** + * @return array{prompt_tokens: int, completion_tokens: int, total_tokens: int, requests: int} + */ + public function totalSummary(?string $model = null): array + { + return $this->summary('total_summary', null, $model); + } + + /** + * @return array{prompt_tokens: int, completion_tokens: int, total_tokens: int, requests: int} + */ + private function summary(string $suffix, ?CarbonImmutable $from, ?string $model = null): array + { + return $this->remember($suffix.'_'.($model ?? 'all'), function () use ($from, $model) { + $rows = AiModelDailyUsage::query() + ->when($from, fn (Builder $query) => $query->where('date', '>=', $from)) + ->when($model === self::OTHER_MODEL, fn (Builder $query) => $query->whereNull('ai_model_id')) + ->when($model && $model !== self::OTHER_MODEL, fn (Builder $query) => $query->where('model', $model)) + ->get(); + + return [ + 'prompt_tokens' => (int) $rows->sum('prompt_tokens'), + 'completion_tokens' => (int) $rows->sum('completion_tokens'), + 'total_tokens' => (int) $rows->sum('total_tokens'), + 'requests' => (int) $rows->sum('requests'), + ]; + }); + } + + private function remember(string $suffix, callable $callback): mixed + { + $version = Cache::get(self::VERSION_CACHE_KEY, 0); + $key = Str::slug("llm_usage_{$version}_{$suffix}", '_'); + + return Cache::remember($key, now()->addHour(), $callback); + } +} diff --git a/app/Actions/StoreLlmUsageAction.php b/app/Actions/StoreLlmUsageAction.php new file mode 100644 index 0000000..1609566 --- /dev/null +++ b/app/Actions/StoreLlmUsageAction.php @@ -0,0 +1,42 @@ + $rows + */ + public function store(Collection $rows): int + { + if ($rows->isEmpty()) { + return 0; + } + + $aiModelIds = AiModel::pluck('id', 'name'); + + $rows = $rows->map(fn (array $row) => [ + ...$row, + 'ai_model_id' => $aiModelIds->get($row['model']), + ]); + + $count = AiModelDailyUsage::upsert( + $rows->all(), + uniqueBy: ['date', 'model'], + update: ['ai_model_id', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'requests', 'spend'], + ); + + Cache::increment(LlmUsageStatsAction::VERSION_CACHE_KEY); + + Artisan::call(ClearCommand::class); + + return $count; + } +} diff --git a/app/Actions/ViewDataAction.php b/app/Actions/ViewDataAction.php index 30ef845..36cc56a 100644 --- a/app/Actions/ViewDataAction.php +++ b/app/Actions/ViewDataAction.php @@ -6,7 +6,6 @@ use App\Enums\AiModelCategoryEnum; use App\Enums\ContactSectionEnum; use App\Models\AiModel; -use App\Models\Configuration; use App\Models\Contact; use App\Models\News; use App\Models\OpenSource; @@ -19,15 +18,6 @@ class ViewDataAction { - public function configuration(string $locale): ?Configuration - { - $key = Str::slug("configuration_{$locale}"); - - return Cache::rememberForever($key, function () { - return Configuration::first(); - }); - } - public function products(string $locale): Collection { $key = Str::slug("products_published_{$locale}"); diff --git a/app/Console/Commands/FetchLlmAnalyticsCommand.php b/app/Console/Commands/FetchLlmAnalyticsCommand.php new file mode 100644 index 0000000..e42ebf9 --- /dev/null +++ b/app/Console/Commands/FetchLlmAnalyticsCommand.php @@ -0,0 +1,48 @@ +date($this->option('to')) ?? CarbonImmutable::today(); + $from = $this->date($this->option('from')) + ?? ($this->option('full') ? $this->date(self::INITIAL_SYNC_START) : $to->subDays(3)); + + if ($from->greaterThan($to)) { + $this->error('The --from date must not be after the --to date.'); + + return self::FAILURE; + } + + $days = collect(CarbonPeriod::create($from, $to)); + + $days->each(fn (mixed $day) => FetchLlmUsageJob::dispatch(CarbonImmutable::instance($day))); + + $this->info("Dispatched {$days->count()} job(s) for {$from->toDateString()} to {$to->toDateString()}."); + + return self::SUCCESS; + } + + private function date(?string $value): ?CarbonImmutable + { + return $value === null + ? null + : CarbonImmutable::createFromFormat('!Y-m-d', $value); + } +} diff --git a/app/Helpers/HelperDate.php b/app/Helpers/HelperDate.php index 62d4999..d5a2601 100755 --- a/app/Helpers/HelperDate.php +++ b/app/Helpers/HelperDate.php @@ -19,4 +19,18 @@ public function formatDate(Carbon $date): string { return $date->format('d.m.Y'); } + + public function monthLabel(string $yearMonth): string + { + return Carbon::createFromFormat('!Y-m', $yearMonth) + ->locale(app()->getLocale()) + ->translatedFormat('F Y'); + } + + public function monthName(int $month): string + { + return Carbon::create(month: $month) + ->locale(app()->getLocale()) + ->translatedFormat('F'); + } } diff --git a/app/Helpers/HelperNumber.php b/app/Helpers/HelperNumber.php index 1c3dbca..c0fc1af 100755 --- a/app/Helpers/HelperNumber.php +++ b/app/Helpers/HelperNumber.php @@ -2,6 +2,8 @@ namespace App\Helpers; +use Illuminate\Support\Number; + class HelperNumber { public function format(int|float|null $number, int $decimals = 2, string $decimalSeparator = '.', string $thousandSeparator = "'"): string @@ -11,6 +13,13 @@ public function format(int|float|null $number, int $decimals = 2, string $decima : self::defaultValue($decimals); } + public function abbreviate(int|float|null $number, int $maxPrecision = 1): string + { + return $number >= 1_000_000 + ? Number::abbreviate($number, maxPrecision: $maxPrecision) + : $this->format($number, 0); + } + protected static function defaultValue(int $decimals = 0): string { return number_format(0, $decimals); diff --git a/app/Http/Controllers/Ai/AiIndexController.php b/app/Http/Controllers/Ai/AiIndexController.php index 9a4ccf3..3cab038 100644 --- a/app/Http/Controllers/Ai/AiIndexController.php +++ b/app/Http/Controllers/Ai/AiIndexController.php @@ -2,16 +2,18 @@ namespace App\Http\Controllers\Ai; +use App\Actions\LlmUsageStatsAction; use App\Actions\PageAction; use App\Http\Controllers\Controller; use Illuminate\View\View; class AiIndexController extends Controller { - public function __invoke(): View + public function __invoke(LlmUsageStatsAction $stats): View { return view('app.ai.index')->with([ 'page' => (new PageAction(locale: null, routeName: 'ai.index'))->default(), + 'llmSummary' => $stats->currentMonthSummary(), ]); } } diff --git a/app/Http/Controllers/Ai/AiLlmAnalyticsIndexController.php b/app/Http/Controllers/Ai/AiLlmAnalyticsIndexController.php new file mode 100644 index 0000000..6940e55 --- /dev/null +++ b/app/Http/Controllers/Ai/AiLlmAnalyticsIndexController.php @@ -0,0 +1,90 @@ +models(); + $years = $stats->years(); + $monthOptions = collect(range(1, 12))->mapWithKeys(fn (int $month) => [ + str_pad((string) $month, 2, '0', STR_PAD_LEFT) => HelperDate::monthName($month), + ]); + + $otherLabel = __('components.ai_llm_analytics.filter.other_models'); + $modelOptions = $stats->hasOtherModels() ? $models->concat([$otherLabel]) : $models; + + $year = $years->first(fn (string $option) => $option === $request->query('year')); + $month = $this->resolveMonth($monthOptions, (string) $request->query('month')); + $model = $this->resolveModel($models, $otherLabel, (string) $request->query('model'), $stats->hasOtherModels()); + + $breakdown = $stats->monthlyBreakdown($year, $month, $model)->reverse()->values(); + + $page = Paginator::resolveCurrentPage(); + + $periods = new LengthAwarePaginator( + items: $breakdown->forPage($page, self::PER_PAGE)->values(), + total: $breakdown->count(), + perPage: self::PER_PAGE, + currentPage: $page, + options: [ + 'path' => Paginator::resolveCurrentPath(), + 'query' => array_filter(['year' => $year, 'month' => $month, 'model' => $model]), + ], + ); + + return view('app.ai.llm.analytics')->with([ + 'page' => (new PageAction(locale: null, routeName: 'ai.llm.analytics.index'))->default(), + 'monthSummary' => $stats->currentMonthSummary($model), + 'yearSummary' => $stats->currentYearSummary($model), + 'totalSummary' => $stats->totalSummary($model), + 'periods' => $periods, + 'grandTotal' => [ + 'prompt_tokens' => (int) $breakdown->sum('prompt_tokens'), + 'completion_tokens' => (int) $breakdown->sum('completion_tokens'), + 'total_tokens' => (int) $breakdown->sum('total_tokens'), + 'requests' => (int) $breakdown->sum('requests'), + ], + 'modelOptions' => $modelOptions, + 'years' => $years, + 'monthOptions' => $monthOptions, + 'year' => $year, + 'month' => $month, + 'model' => $model, + 'modelLabel' => $model === LlmUsageStatsAction::OTHER_MODEL ? $otherLabel : $model, + ]); + } + + private function resolveModel(Collection $models, string $otherLabel, string $input, bool $hasOther): ?string + { + if ($hasOther && (strcasecmp($input, $otherLabel) === 0 || strcasecmp($input, LlmUsageStatsAction::OTHER_MODEL) === 0)) { + return LlmUsageStatsAction::OTHER_MODEL; + } + + return $models->first(fn (string $option) => strcasecmp($option, $input) === 0); + } + + private function resolveMonth(Collection $monthOptions, string $input): ?string + { + if ($monthOptions->has($input)) { + return $input; + } + + $key = $monthOptions->search(fn (string $label) => strcasecmp($label, $input) === 0); + + return $key === false ? null : $key; + } +} diff --git a/app/Http/Controllers/Sitemap/SitemapController.php b/app/Http/Controllers/Sitemap/SitemapController.php index f292615..e89ea05 100644 --- a/app/Http/Controllers/Sitemap/SitemapController.php +++ b/app/Http/Controllers/Sitemap/SitemapController.php @@ -30,6 +30,7 @@ class SitemapController extends Controller 'legal.privacy.index', 'ai.index', 'ai.llm.index', + 'ai.llm.analytics.index', ]; protected const array DEFAULT_LOCALES = [ diff --git a/app/Http/Controllers/Start/StartIndexController.php b/app/Http/Controllers/Start/StartIndexController.php index ab94913..27cd7c6 100644 --- a/app/Http/Controllers/Start/StartIndexController.php +++ b/app/Http/Controllers/Start/StartIndexController.php @@ -4,7 +4,6 @@ use App\Actions\LocaleAction; use App\Actions\PageAction; -use App\Actions\ViewDataAction; use App\Enums\LocaleEnum; use App\Http\Controllers\Controller; use Illuminate\Support\Str; @@ -25,7 +24,6 @@ public function __invoke(): View return view('app.start.index')->with([ 'page' => (new PageAction(locale: null, routeName: 'start.index'))->default(), - 'news' => (new ViewDataAction)->news($locale), ]); } } diff --git a/app/Jobs/FetchLlmUsageJob.php b/app/Jobs/FetchLlmUsageJob.php new file mode 100644 index 0000000..1f42f8c --- /dev/null +++ b/app/Jobs/FetchLlmUsageJob.php @@ -0,0 +1,31 @@ + + */ + public function backoff(): array + { + return [60, 300]; + } + + public function handle(FetchLlmUsageAction $fetch, StoreLlmUsageAction $store): void + { + $store->store($fetch->fetchDay($this->date)); + } +} diff --git a/app/Models/AiModel.php b/app/Models/AiModel.php index 0eb689f..c5f4cee 100644 --- a/app/Models/AiModel.php +++ b/app/Models/AiModel.php @@ -5,6 +5,7 @@ use App\Enums\AiModelCategoryEnum; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; class AiModel extends Model { @@ -19,6 +20,11 @@ public function replacedBy(): BelongsTo return $this->belongsTo(AiModel::class, 'replaced_by_id'); } + public function dailyUsages(): HasMany + { + return $this->hasMany(AiModelDailyUsage::class); + } + public function localizedRole(): ?string { return $this->role[substr(app()->getLocale(), 0, 2)] ?? null; diff --git a/app/Models/AiModelDailyUsage.php b/app/Models/AiModelDailyUsage.php new file mode 100644 index 0000000..34d6ae0 --- /dev/null +++ b/app/Models/AiModelDailyUsage.php @@ -0,0 +1,22 @@ + 'date', + 'spend' => 'decimal:6', + ]; + + public function aiModel(): BelongsTo + { + return $this->belongsTo(AiModel::class); + } +} diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php deleted file mode 100644 index bab2bd9..0000000 --- a/app/Models/Configuration.php +++ /dev/null @@ -1,21 +0,0 @@ - 'json', - 'section_news' => 'boolean', - 'section_services' => 'boolean', - 'section_products' => 'boolean', - 'section_technologies' => 'boolean', - 'section_open_source' => 'boolean', - 'links' => 'json', - ]; -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 37c4d3c..e6683db 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,16 +2,13 @@ namespace App\Providers; -use App\Actions\ViewDataAction; use App\Checks\FailedJobsCheck; use App\Checks\FilesystemsDefaultCheck; use App\Checks\JobsCheck; -use App\Models\Configuration; use App\Models\News; use App\Models\Product; use App\Models\Service; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Spatie\Health\Checks\Checks\CacheCheck; use Spatie\Health\Checks\Checks\DebugModeCheck; @@ -44,8 +41,6 @@ public function boot(): void FailedJobsCheck::new(), SecurityAdvisoriesCheck::new()->lastDayOfMonth(), ]); - - View::share('configuration', $this->getConfiguration()); } private function multilanguage(): void @@ -54,13 +49,4 @@ private function multilanguage(): void Service::registerLocalizedBinding('service'); Product::registerLocalizedBinding('product'); } - - private function getConfiguration(): ?Configuration - { - try { - return (new ViewDataAction)->configuration(app()->getLocale()); - } catch (\Throwable) { - return null; - } - } } diff --git a/app/Policies/NewsPolicy.php b/app/Security/Policies/NewsPolicy.php similarity index 97% rename from app/Policies/NewsPolicy.php rename to app/Security/Policies/NewsPolicy.php index 88cccac..8e98db6 100644 --- a/app/Policies/NewsPolicy.php +++ b/app/Security/Policies/NewsPolicy.php @@ -1,6 +1,6 @@ Str::slug($locale), 'page' => $this->page, 'preconnectCloudinary' => $this->preconnectCloudinary, - 'services' => (new ViewDataAction)->services($locale), - 'products' => (new ViewDataAction)->products($locale), ]); } } diff --git a/config/app.php b/config/app.php index 111d1f4..170521e 100644 --- a/config/app.php +++ b/config/app.php @@ -13,7 +13,7 @@ | */ - 'name' => env('APP_NAME', 'paperflakes AG'), + 'name' => env('APP_NAME', 'codebar Solutions AG'), /* |-------------------------------------------------------------------------- diff --git a/config/seeder.php b/config/seeder.php deleted file mode 100644 index 907587c..0000000 --- a/config/seeder.php +++ /dev/null @@ -1,8 +0,0 @@ - [ - 'paperflakes' => env('SEEDER_PAPERFLAKES', false), - 'codebar' => env('SEEDER_CODEBAR', false), - ], -]; diff --git a/config/services.php b/config/services.php index bf14da3..c09eb11 100644 --- a/config/services.php +++ b/config/services.php @@ -31,6 +31,11 @@ ], ], + 'litellm' => [ + 'url' => env('LITELLM_URL', 'https://llm.codebar.net'), + 'master_key' => env('LITELLM_MASTER_KEY'), + ], + 'microsoft' => [ 'client_id' => env('MICROSOFT_CLIENT_ID'), 'client_secret' => env('MICROSOFT_CLIENT_SECRET'), diff --git a/database/factories/AiModelDailyUsageFactory.php b/database/factories/AiModelDailyUsageFactory.php new file mode 100644 index 0000000..56d1b72 --- /dev/null +++ b/database/factories/AiModelDailyUsageFactory.php @@ -0,0 +1,33 @@ + + */ +class AiModelDailyUsageFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $promptTokens = fake()->numberBetween(1_000, 5_000_000); + $completionTokens = fake()->numberBetween(1_000, 1_000_000); + + return [ + 'date' => fake()->unique()->dateTimeBetween('-1 year', 'now')->format('Y-m-d'), + 'model' => fake()->randomElement(['qwen3.6:35b', 'qwen3-coder:30b', 'qwen3-embedding:4b', 'gemma4:31b']), + 'prompt_tokens' => $promptTokens, + 'completion_tokens' => $completionTokens, + 'total_tokens' => $promptTokens + $completionTokens, + 'requests' => fake()->numberBetween(1, 2_000), + 'spend' => fake()->randomFloat(6, 0, 10), + ]; + } +} diff --git a/database/factories/ConfigurationFactory.php b/database/factories/ConfigurationFactory.php deleted file mode 100644 index 7c0f09a..0000000 --- a/database/factories/ConfigurationFactory.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ -class ConfigurationFactory extends Factory -{ - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - // - ]; - } -} diff --git a/database/files/intro/paperflakes_intro_de.md b/database/files/intro/paperflakes_intro_de.md deleted file mode 100644 index 9944506..0000000 --- a/database/files/intro/paperflakes_intro_de.md +++ /dev/null @@ -1,5 +0,0 @@ -

Wer wir sind

-

Bei paperflakes unterstützen wir dich dabei, deine Informationsflüsse zu strukturieren und Dokumentenprozesse digital abzubilden – effizient, sicher und zukunftsorientiert. Wir beraten dich individuell, denken ganzheitlich und setzen Lösungen um, die exakt zu deinen Anforderungen passen. Unser Ziel: messbarer Mehrwert im Alltag – durch mehr Transparenz, klare Abläufe und spürbare Entlastung bei der täglichen Arbeit.

- -

Wie wir arbeiten

-

Am Anfang nehmen wir uns Zeit, deine Abläufe, Herausforderungen und Ziele genau zu verstehen. Denn nur wer wirklich durchblickt, kann sinnvolle und nachhaltige Lösungen entwickeln. Gemeinsam mit dir erstellen wir ein Umsetzungskonzept, das auf deine konkreten Anforderungen abgestimmt ist – fachlich fundiert und praxisnah. Auf dieser Basis erhältst du ein klar strukturiertes Angebot. Wenn du dich dafür entscheidest, setzen wir die Lösung gemeinsam mit dir um – von der Planung bis zur erfolgreichen Einführung.

diff --git a/database/files/intro/paperflakes_intro_en.md b/database/files/intro/paperflakes_intro_en.md deleted file mode 100644 index c96dc38..0000000 --- a/database/files/intro/paperflakes_intro_en.md +++ /dev/null @@ -1,5 +0,0 @@ -

Who we are

-

At paperflakes, we help you structure your information flows and digitise document-related processes – efficiently, securely, and with a future-focused approach. We offer tailored consulting, take a holistic perspective, and implement solutions that fit your specific requirements. Our goal: measurable added value in your day-to-day operations – through greater transparency, streamlined processes, and noticeable relief in your daily workload.

- -

How we work

-

We start by taking the time to fully understand your workflows, challenges, and goals. Because only with real insight can effective and sustainable solutions be created. Together, we develop an implementation concept that is tailored to your specific needs – technically sound and grounded in real-world practice. Based on this, you'll receive a clearly structured proposal. If you decide to proceed, we'll implement the solution together – from planning to successful rollout.

diff --git a/database/files/services/de_CH/docuware.md b/database/files/services/de_CH/docuware.md deleted file mode 100644 index 63457d9..0000000 --- a/database/files/services/de_CH/docuware.md +++ /dev/null @@ -1,26 +0,0 @@ -Als offizieller **DocuWare-Partner** unterstützt **paperflakes AG** Unternehmen dabei, ihre zentralen Geschäftsprozesse zu digitalisieren, zu automatisieren und zu vereinfachen. - -Unser Fokus? **Technisch anspruchsvolle Integrationen** – richtig umgesetzt. Wir implementieren DocuWare nicht nur – wir integrieren es nahtlos in Ihre bestehende IT-Landschaft. Individuell. Skalierbar. Zukunftssicher. - -## Was ist DocuWare? - -**DocuWare** ist eine leistungsstarke Cloud-Plattform für **Dokumentenmanagement** und **Workflow-Automatisierung**. Sie hilft Ihnen dabei: - -- Dokumente digital zu erfassen, zu organisieren und sicher zu archivieren -- Geschäftsprozesse effizient zu automatisieren -- Jederzeit und von überall sicher auf Informationen zuzugreifen -- Compliance- und Datenschutzanforderungen problemlos zu erfüllen - -Ob Rechnungen, Verträge, HR-Unterlagen oder Kundendaten – DocuWare sorgt für Struktur, Übersicht und Kontrolle. - -## Warum paperflakes AG + DocuWare? - -- **Technisches Know-how** – Wir verbinden DocuWare mit komplexen Systemen wie ERP, Buchhaltung oder CRM -- **Datenschutz im Fokus** – So wenig Drittanbieter wie möglich, für eine schlanke und DSGVO-konforme Umgebung -- **Effizientere Abläufe** – Weniger manuelle Arbeit, schnellere Prozesse -- **Alles aus einer Hand** – Beratung, Umsetzung und Support -- **Zukunftssicher** – Lösungen, die mit Ihrem Unternehmen mitwachsen – vom KMU bis zum Grossunternehmen - -## Bereit für bessere Dokumentenprozesse? - -Wir glauben an smarte, einfache und sichere Lösungen, die Ihre Arbeit erleichtern – nicht verkomplizieren. **Mit DocuWare und paperflakes AG wird Ihr Arbeitsalltag effizienter.** \ No newline at end of file diff --git a/database/files/services/en_CH/docuware.md b/database/files/services/en_CH/docuware.md deleted file mode 100644 index 5913fef..0000000 --- a/database/files/services/en_CH/docuware.md +++ /dev/null @@ -1,26 +0,0 @@ -As an official **DocuWare partner**, **paperflakes AG** empowers businesses to digitize, automate, and simplify their most important processes. - -Our focus? **Complex technical integrations** done right. We don’t just implement DocuWare—we integrate it seamlessly into your existing IT environment. Custom-fit. Scalable. Built to last. - -## What is DocuWare? - -**DocuWare** is a powerful cloud-based platform for **document management** and **workflow automation**. It helps you: - -- Digitally capture, organize, and store documents -- Automate your day-to-day business workflows -- Access documents securely—anytime, anywhere -- Stay compliant with strict audit and privacy requirements - -Whether you’re handling invoices, contracts, HR files, or customer records—DocuWare keeps everything organized, accessible, and under control. - -## Why choose paperflakes AG + DocuWare? - -- **Technical expertise** – We specialize in integrating DocuWare with complex systems like ERP, finance, and CRM -- **Privacy first** – We avoid unnecessary third-party tools to keep your landscape clean, secure, and GDPR-friendly -- **Smarter workflows** – Free your team from repetitive tasks and speed up processing times -- **One partner for everything** – From planning to implementation to long-term support -- **Future-proof** – Scalable solutions for SMEs and larger enterprises alike - -## Ready to streamline your document processes? - -We believe in smart, simple, and secure solutions that work for your team—not the other way around. **Let DocuWare and paperflakes AG simplify your everyday work.** \ No newline at end of file diff --git a/database/migrations/2026_07_21_150000_drop_configurations_table.php b/database/migrations/2026_07_21_150000_drop_configurations_table.php new file mode 100644 index 0000000..53b6d62 --- /dev/null +++ b/database/migrations/2026_07_21_150000_drop_configurations_table.php @@ -0,0 +1,43 @@ +id(); + + $table->string('company')->nullable(); + $table->string('company_primary_color')->nullable(); + + $table->json('component_intro'); + + $table->boolean('section_news')->default(false); + $table->boolean('section_services')->default(false); + $table->boolean('section_products')->default(false); + $table->boolean('section_technologies')->default(false); + $table->boolean('section_open_source')->default(false); + + $table->string('key'); + + $table->json('links')->nullable(); + + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2026_07_21_160000_create_ai_model_daily_usages_table.php b/database/migrations/2026_07_21_160000_create_ai_model_daily_usages_table.php new file mode 100644 index 0000000..988983c --- /dev/null +++ b/database/migrations/2026_07_21_160000_create_ai_model_daily_usages_table.php @@ -0,0 +1,37 @@ +id(); + $table->date('date'); + $table->string('model'); + $table->foreignId('ai_model_id')->nullable()->constrained('ai_models')->nullOnDelete(); + $table->unsignedBigInteger('prompt_tokens')->default(0); + $table->unsignedBigInteger('completion_tokens')->default(0); + $table->unsignedBigInteger('total_tokens')->default(0); + $table->unsignedInteger('requests')->default(0); + $table->decimal('spend', 12, 6)->default(0); + $table->timestamps(); + $table->unique(['date', 'model']); + $table->index('model'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ai_model_daily_usages'); + } +}; diff --git a/database/seeders/Codebar/ConfigurationsTableSeeder.php b/database/seeders/Codebar/ConfigurationsTableSeeder.php deleted file mode 100644 index cf442f0..0000000 --- a/database/seeders/Codebar/ConfigurationsTableSeeder.php +++ /dev/null @@ -1,40 +0,0 @@ - 'codebar Solutions AG', - 'company_primary_color' => '#500472', - - 'component_intro' => [ - LocaleEnum::DE->value => file_get_contents(database_path('files/intro/codebar_intro_de.md')), - LocaleEnum::EN->value => file_get_contents(database_path('files/intro/codebar_intro_en.md')), - ], - - 'section_news' => false, - 'section_services' => false, - 'section_products' => false, - 'section_technologies' => false, - 'section_open_source' => false, - - 'key' => '_codebar', - - 'links' => [ - 'linkedin' => 'https://www.linkedin.com/company/codebarag', - 'github' => 'https://github.com/orgs/codebar-ag', - ], - ]); - } -} diff --git a/database/seeders/Codebar/PagesTableSeeder.php b/database/seeders/Codebar/PagesTableSeeder.php index e2bb115..92f12ac 100644 --- a/database/seeders/Codebar/PagesTableSeeder.php +++ b/database/seeders/Codebar/PagesTableSeeder.php @@ -152,6 +152,18 @@ private function enCH() 'description' => 'These are the local open-source models we currently rely on — all of them run on our own infrastructure, in our very own office basement.', ] ); + + Page::updateOrCreate( + [ + 'key' => 'ai.llm.analytics.index', + 'locale' => $locale, + ], + [ + 'robots' => 'index,follow', + 'title' => 'LLM usage analytics', + 'description' => 'Token usage and request statistics of our locally hosted LLMs — aggregated per month and per model.', + ] + ); } private function deCH() @@ -289,5 +301,17 @@ private function deCH() 'description' => 'Auf diese lokalen Open-Source-Modelle setzen wir aktuell — sie laufen alle auf unserer eigenen Infrastruktur, im hauseigenen Bürokeller.', ] ); + + Page::updateOrCreate( + [ + 'key' => 'ai.llm.analytics.index', + 'locale' => $locale, + ], + [ + 'robots' => 'index,follow', + 'title' => 'LLM-Nutzungsstatistik', + 'description' => 'Token-Verbrauch und Anfrage-Statistiken unserer lokal betriebenen LLMs — aggregiert pro Monat und Modell.', + ] + ); } } diff --git a/database/seeders/CodebarSeeder.php b/database/seeders/CodebarSeeder.php index c7c57a0..3ebee1b 100644 --- a/database/seeders/CodebarSeeder.php +++ b/database/seeders/CodebarSeeder.php @@ -3,7 +3,6 @@ namespace Database\Seeders; use Database\Seeders\Codebar\AiModelsTableSeeder; -use Database\Seeders\Codebar\ConfigurationsTableSeeder; use Database\Seeders\Codebar\ContactsTableSeeder; use Database\Seeders\Codebar\OpenSourceTableSeeder; use Database\Seeders\Codebar\PagesTableSeeder; @@ -19,7 +18,6 @@ class CodebarSeeder extends Seeder */ public function run(): void { - $this->call(ConfigurationsTableSeeder::class); $this->call(PagesTableSeeder::class); $this->call(ContactsTableSeeder::class); // $this->call(OpenSourceTableSeeder::class); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 72a5680..a0ae388 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -5,9 +5,7 @@ use App\Enums\LocaleEnum; use App\Enums\RoleEnum; use App\Models\User; -use Database\Seeders\Paperflakes\RolesAndPermissionsSeeder; use Illuminate\Database\Seeder; -use Illuminate\Support\Facades\Config; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; @@ -31,14 +29,6 @@ public function run(): void $user->assignRole(RoleEnum::ADMINISTRATOR, RoleEnum::USER); - if (Config::get('seeder.seeder.paperflakes')) { - // php artisan db:seed --class=Database\\Seeders\\PaperflakesSeeder --force - $this->call(PaperflakesSeeder::class); - } - if (Config::get('seeder.seeder.codebar')) { - // php artisan db:seed --class=Database\\Seeders\\CodebarSeeder --force - $this->call(CodebarSeeder::class); - } - + $this->call(CodebarSeeder::class); } } diff --git a/database/seeders/Paperflakes/ConfigurationsTableSeeder.php b/database/seeders/Paperflakes/ConfigurationsTableSeeder.php deleted file mode 100644 index 9d1af50..0000000 --- a/database/seeders/Paperflakes/ConfigurationsTableSeeder.php +++ /dev/null @@ -1,40 +0,0 @@ - 'paperflakes AG', - 'company_primary_color' => '#69b3a1', - - 'component_intro' => [ - LocaleEnum::DE->value => file_get_contents(database_path('files/intro/paperflakes_intro_de.md')), - LocaleEnum::EN->value => file_get_contents(database_path('files/intro/paperflakes_intro_en.md')), - ], - - 'section_news' => true, - 'section_services' => true, - 'section_products' => true, - 'section_technologies' => false, - 'section_open_source' => false, - - 'key' => '_paperflakes', - - 'links' => [ - 'linkedin' => 'https://www.linkedin.com/company/paperflakes', - 'github' => 'https://github.com/orgs/paperflakes-ag', - ], - - ]); - } -} diff --git a/database/seeders/Paperflakes/ContactsTableSeeder.php b/database/seeders/Paperflakes/ContactsTableSeeder.php deleted file mode 100644 index ae29ee5..0000000 --- a/database/seeders/Paperflakes/ContactsTableSeeder.php +++ /dev/null @@ -1,195 +0,0 @@ - 1], - [ - 'name' => 'Sebastian Bürgin-Fix', - 'published' => true, - 'sections' => [ - ContactSectionEnum::EMPLOYEES => [ - 'key' => ContactSectionEnum::EMPLOYEES, - 'role' => [ - 'de_CH' => 'Software-Architekt', - 'en_CH' => 'Software-Engineer', - ], - ], - ContactSectionEnum::BOARD_MEMBERS => [ - 'key' => ContactSectionEnum::BOARD_MEMBERS, - ], - ], - 'icons' => [ - 'email' => 'sebastian.buergin@paperflakes.ch', - 'linkedin' => 'https://www.linkedin.com/in/fix-sebastian/', - ], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/s_fix_e_background_removal_f_png.webp', - ] - ); - - Contact::updateOrCreate( - ['id' => 2], - [ - 'name' => 'Mischa Lanz', - 'published' => true, - 'sections' => [ - ContactSectionEnum::EMPLOYEES => [ - 'key' => ContactSectionEnum::EMPLOYEES, - 'role' => [ - 'de_CH' => 'zunscan.ch', - 'en_CH' => 'zunscan.ch', - ], - ], - ContactSectionEnum::BOARD_MEMBERS => [ - 'key' => ContactSectionEnum::BOARD_MEMBERS, - ], - ], - 'icons' => [ - 'email' => 'mischa.lanz@paperflakes.ch', - 'linkedin' => 'https://www.linkedin.com/in/mischa-lanz-672a65112', - ], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/6528f7c6bddf8430fb5d154c_Mischa_Hemd.webp', - ] - ); - - Contact::updateOrCreate( - ['id' => 3], - [ - 'name' => 'Dominique Ernst', - 'published' => true, - 'sections' => [ - /* ContactSectionEnum::EMPLOYEES => [ - 'key' => ContactSectionEnum::EMPLOYEES, - 'role' => [ - 'de_CH' => 'DMS/ECM Berater', - 'en_CH' => 'DMS/ECM Consultant', - ], - ], */ - ContactSectionEnum::BOARD_MEMBERS => [ - 'key' => ContactSectionEnum::BOARD_MEMBERS, - ], - ], - 'icons' => [ - 'email' => 'dominique.ernst@paperflakes.ch', - 'linkedin' => 'https://www.linkedin.com/in/dominique-ernst', - ], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/ujywqubl5rkkm5hjqjsa_e_background_removal_f_png.webp', - ] - ); - - Contact::updateOrCreate( - ['id' => 4], - [ - 'name' => 'Katja Lanz', - 'published' => true, - 'sections' => [ - ContactSectionEnum::EMPLOYEES => [ - 'key' => ContactSectionEnum::EMPLOYEES, - 'role' => [ - 'de_CH' => 'HR', - 'en_CH' => 'HR', - ], - ], - ], - 'icons' => [ - 'email' => 'katja.lanz@paperflakes.ch', - 'linkedin' => 'https://www.linkedin.com/in/katja-lanz-a92372149/', - ], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/652d4179a3494dedacf6555a_Katja_Jacke.webp', - ] - ); - - Contact::updateOrCreate( - ['id' => 5], - [ - 'name' => 'Melanie Bürgin-Fix', - 'published' => true, - 'sections' => [ - ContactSectionEnum::EMPLOYEES => [ - 'key' => ContactSectionEnum::EMPLOYEES, - 'role' => [ - 'de_CH' => 'Administration', - 'en_CH' => 'Administration', - ], - ], - ], - 'icons' => [ - 'email' => 'melanie.buergin@paperflakes.ch', - 'linkedin' => 'https://www.linkedin.com/in/melanie-buergin', - ], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/paperflakes.webp', - ] - ); - - Contact::updateOrCreate( - ['id' => 6], - [ - 'name' => 'DR-G2110', - 'published' => true, - 'sections' => [ - ContactSectionEnum::EMPLOYEES => [ - 'key' => ContactSectionEnum::EMPLOYEES, - 'role' => [ - 'de_CH' => 'Digitalisierungs-Beauftragter', - 'en_CH' => 'Head of Digital Transformation', - ], - ], - ], - 'icons' => [], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/drg_e_background_removal_f_png.webp', - ] - ); - - Contact::updateOrCreate( - ['id' => 6], - [ - 'name' => 'PST GmbH', - 'published' => false, - 'sections' => [ - ContactSectionEnum::COLLABORATIONS => [ - 'key' => ContactSectionEnum::COLLABORATIONS, - 'role' => [ - 'de_CH' => 'Finanzen', - 'en_CH' => 'Finance', - ], - ], - ], - 'icons' => [ - 'email' => 'info@pstgmbh.ch', - 'website' => 'https://www.pstgmbh.ch', - ], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/paperflakes.webp', - ] - ); - - Contact::updateOrCreate( - ['id' => 7], - [ - 'name' => 'Dario Wieland', - 'published' => true, - 'sections' => [ - ContactSectionEnum::COLLABORATIONS => [ - 'key' => ContactSectionEnum::COLLABORATIONS, - 'role' => [ - 'de_CH' => 'Wieland Business Solutions GmbH', - 'en_CH' => 'Wieland Business Solutions GmbH', - ], - ], - ], - 'icons' => [ - 'email' => 'wieland@business-solutions.gmbh', - 'website' => 'https://www.business-solutions.gmbh', - ], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/wds.jpg', - ] - ); - } -} diff --git a/database/seeders/Paperflakes/NewsTableSeeder.php b/database/seeders/Paperflakes/NewsTableSeeder.php deleted file mode 100644 index c73169b..0000000 --- a/database/seeders/Paperflakes/NewsTableSeeder.php +++ /dev/null @@ -1,75 +0,0 @@ -seed( - publishedAt: Carbon::parse('2025-04-06'), - author: 'Sebastian Bürgin-Fix', - localizedData: [ - 'de_CH' => [ - 'title' => 'DocuWare 7.12 ist da', - 'slug' => 'docuware-7-12-ist-da', - 'teaser' => 'Mehr Automatisierung, mehr Insights, mehr Effizienz', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - 'content' => file_get_contents(database_path('files/news/de_CH/20250406_docuware_712.md')), - 'tags' => ['DMS/ECM', 'DocuWare'], - ], - 'en_CH' => [ - 'title' => 'DocuWare 7.12 is here', - 'slug' => 'docuware-7-12-is-here', - 'teaser' => 'More automation, more insights, more efficiency', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - 'content' => file_get_contents(database_path('files/news/en_CH/20250406_docuware_712.md')), - 'tags' => ['DMS/ECM', 'DocuWare'], - - ], - ] - ); - } - - private function seed(Carbon $publishedAt, string $author, array $localizedData): void - { - $entries = collect($localizedData)->map(function ($data, $locale) use ($author, $publishedAt) { - $slug = Str::slug(Arr::get($data, 'slug'), '-', $locale); - - return News::updateOrCreate( - [ - 'locale' => $locale, - 'slug' => $slug, - ], - [ - 'author' => $author, - 'published_at' => $publishedAt, - 'title' => Arr::get($data, 'title'), - 'teaser' => Arr::get($data, 'teaser'), - 'image' => Arr::get($data, 'image'), - 'tags' => Arr::get($data, 'tags', []), - 'content' => Arr::get($data, 'content'), - ] - ); - }); - - $entries->each(function (News $entry) use ($entries) { - $entries->each(function (News $reference) use ($entry) { - $entry->references()->updateOrCreate([ - 'reference_type' => get_class($reference), - 'reference_id' => $reference->id, - 'reference_locale' => $reference->locale, - ]); - }); - }); - } -} diff --git a/database/seeders/Paperflakes/PagesTableSeeder.php b/database/seeders/Paperflakes/PagesTableSeeder.php deleted file mode 100644 index fb4d5f3..0000000 --- a/database/seeders/Paperflakes/PagesTableSeeder.php +++ /dev/null @@ -1,212 +0,0 @@ -deCH(); - $this->enCH(); - } - - private function enCH() - { - $locale = LocaleEnum::EN->value; - - Page::updateOrCreate( - [ - 'key' => 'start.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Your Digital Partner', - 'description' => 'We support you with smart digital solutions that move your business forward.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'news.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'News & Insights', - 'description' => 'Stay up to date with the latest news, expert insights and trends on DMS, ECM and digital transformation from paperflakes.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'about-us.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'About Us – paperflakes AG', - 'description' => 'Get to know paperflakes AG – your Swiss partner for digital transformation and innovative software solutions.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'services.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Services that Support You', - 'description' => 'From strategy to implementation - we\'re here to support you all the way.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'products.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Tools That Empower You', - 'description' => 'Our products are built to help you work smarter, faster and better.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'legal.imprint.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Legal Notice', - 'description' => 'All legal details about paperflakes AG.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'contact.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Let\'s Talk', - 'description' => 'Have a question? We\'re here to support you - just reach out.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - } - - private function deCH() - { - $locale = LocaleEnum::DE->value; - - Page::updateOrCreate( - [ - 'key' => 'start.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Dein digitaler Partner', - 'description' => 'Wir unterstützen dich mit cleveren Lösungen für deinen digitalen Erfolg.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'news.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Neuigkeiten & Insights', - 'description' => 'Aktuelle News, Fachbeiträge und Trends rund um DMS, ECM und digitale Transformation mit paperflakes.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'about-us.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Über uns – paperflakes AG', - 'description' => 'Lerne die paperflakes AG kennen – dein Schweizer Partner für digitale Transformation und innovative Softwarelösungen.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'services.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Dienstleistungen für dich', - 'description' => 'Von der Strategie bis zur Umsetzung - wir stehen dir zur Seite.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'products.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Tools, die dich stärken', - 'description' => 'Unsere Produkte helfen dir, effizienter, schneller und einfacher zu arbeiten.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'legal.imprint.index', - 'locale' => $locale, - ], - [ - 'robots' => 'index,follow', - 'title' => 'Rechtliches', - 'description' => 'Alle rechtlichen Informationen zur paperflakes AG.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - - Page::updateOrCreate( - [ - 'key' => 'contact.index', - 'locale' => $locale, - ], - [ - - 'robots' => 'index,follow', - 'title' => 'Lass uns sprechen', - 'description' => 'Fragen? Wir sind fürr dich da - melde dich jederzeit bei uns.', - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ] - ); - } -} diff --git a/database/seeders/Paperflakes/ProductsTableSeeder.php b/database/seeders/Paperflakes/ProductsTableSeeder.php deleted file mode 100644 index ee990af..0000000 --- a/database/seeders/Paperflakes/ProductsTableSeeder.php +++ /dev/null @@ -1,90 +0,0 @@ -seed( - order: 1, - sharedSlug: 'docuhub', - localizedData: [ - LocaleEnum::DE->value => [ - 'name' => 'DocuHub', - 'teaser' => 'Bring dein DMS mit smarten Integrationen auf das nächste Level', - 'tags' => ['DocuWare', 'M-Files', 'SharePoint'], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - 'content' => file_get_contents(database_path('files/products/de_CH/docuhub.md')), - ], - LocaleEnum::EN->value => [ - 'name' => 'DocuHub', - 'teaser' => 'Supercharge Your DMS with Smart Integrations', - 'tags' => ['DocuWare', 'M-Files', 'SharePoint'], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - 'content' => file_get_contents(database_path('files/products/en_CH/docuhub.md')), - ], - ] - ); - - $this->seed( - order: 2, - sharedSlug: 'clouddocs', - localizedData: [ - LocaleEnum::DE->value => [ - 'name' => 'CloudDocs', - 'teaser' => 'Gib deinen Kunden sicheren Zugriff auf ihre Dokumente', - 'tags' => ['DocuWare', 'M-Files'], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - 'content' => file_get_contents(database_path('files/products/de_CH/clouddocs.md')), - ], - LocaleEnum::EN->value => [ - 'name' => 'CloudDocs', - 'teaser' => 'Give Your Customers Secure Access to Their Documents', - 'tags' => ['DocuWare', 'M-Files'], - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - 'content' => file_get_contents(database_path('files/products/en_CH/clouddocs.md')), - ], - ] - ); - } - - private function seed(int $order, string $sharedSlug, array $localizedData): void - { - $entries = collect($localizedData)->map(function ($data, $locale) use ($sharedSlug, $order) { - $slug = Str::slug($sharedSlug, '-', $locale); - - return Product::updateOrCreate( - [ - 'locale' => $locale, - 'slug' => $slug, - ], - [ - 'published' => true, - 'order' => $order, - 'name' => Arr::get($data, 'name'), - 'teaser' => Arr::get($data, 'teaser'), - 'tags' => Arr::get($data, 'tags', []), - 'image' => Arr::get($data, 'image'), - 'content' => Arr::get($data, 'content'), - ] - ); - }); - - $entries->each(function (Product $entry) use ($entries) { - $entries->each(function (Product $reference) use ($entry) { - $entry->references()->updateOrCreate([ - 'reference_type' => get_class($reference), - 'reference_id' => $reference->id, - 'reference_locale' => $reference->locale, - ]); - }); - }); - } -} diff --git a/database/seeders/Paperflakes/ServicesTableSeeder.php b/database/seeders/Paperflakes/ServicesTableSeeder.php deleted file mode 100644 index 2c39e42..0000000 --- a/database/seeders/Paperflakes/ServicesTableSeeder.php +++ /dev/null @@ -1,88 +0,0 @@ -seed( - order: 1, - sharedSlug: 'dms-ecm-docuware', - group: 'DMS/ECM', - localizedData: [ - LocaleEnum::DE->value => [ - 'name' => 'DocuWare', - 'teaser' => 'Intelligentes Dokumentenmanagement mit DocuWare', - 'tags' => ['DMS/ECM', 'DocuWare'], - 'content' => file_get_contents(database_path('files/services/de_CH/docuware.md')), - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ], - LocaleEnum::EN->value => [ - 'name' => 'DocuWare', - 'teaser' => 'Smarter Document Management with DocuWare', - 'tags' => ['DMS/ECM', 'DocuWare'], - 'content' => file_get_contents(database_path('files/services/en_CH/docuware.md')), - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ], - ] - ); - - $this->seed( - order: 2, - sharedSlug: 'zunscan-ch', - group: 'Digitalisierung', - localizedData: [ - LocaleEnum::DE->value => [ - 'name' => 'zunscan.ch', - 'teaser' => 'Das Scanning Center in der Nordwestschweiz', - 'tags' => ['Digitalisierung', 'Scanning'], - 'url' => 'https://zunscan.paperflakes.ch', - 'content' => null, - 'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp', - ], - ] - ); - } - - private function seed(int $order, string $sharedSlug, string $group, array $localizedData): void - { - $entries = collect($localizedData)->map(function ($data, $locale) use ($sharedSlug, $order, $group) { - $slug = Str::slug($sharedSlug, '-', $locale); - - return Service::updateOrCreate( - [ - 'locale' => $locale, - 'slug' => $slug, - ], - [ - 'published' => true, - 'order' => $order, - 'group' => $group, - 'name' => Arr::get($data, 'name'), - 'teaser' => Arr::get($data, 'teaser'), - 'tags' => Arr::get($data, 'tags', []), - 'content' => Arr::get($data, 'content'), - 'url' => Arr::get($data, 'url'), - 'image' => Arr::get($data, 'image'), - ] - ); - }); - - $entries->each(function (Service $entry) use ($entries) { - $entries->each(function (Service $reference) use ($entry) { - $entry->references()->updateOrCreate([ - 'reference_type' => get_class($reference), - 'reference_id' => $reference->id, - 'reference_locale' => $reference->locale, - ]); - }); - }); - } -} diff --git a/database/seeders/PaperflakesSeeder.php b/database/seeders/PaperflakesSeeder.php deleted file mode 100644 index d4b66c9..0000000 --- a/database/seeders/PaperflakesSeeder.php +++ /dev/null @@ -1,34 +0,0 @@ -call(ConfigurationsTableSeeder::class); - $this->call(PagesTableSeeder::class); - $this->call(NewsTableSeeder::class); - $this->call(ProductsTableSeeder::class); - $this->call(ServicesTableSeeder::class); - $this->call(ContactsTableSeeder::class); - - if (app()->isLocal()) { - Artisan::call(ClearCommand::class); - } - - } -} diff --git a/database/seeders/Paperflakes/RolesAndPermissionsSeeder.php b/database/seeders/RolesAndPermissionsSeeder.php similarity index 94% rename from database/seeders/Paperflakes/RolesAndPermissionsSeeder.php rename to database/seeders/RolesAndPermissionsSeeder.php index 6d95a55..3535c2a 100644 --- a/database/seeders/Paperflakes/RolesAndPermissionsSeeder.php +++ b/database/seeders/RolesAndPermissionsSeeder.php @@ -1,6 +1,6 @@ [ - 'showme' => [ - 'title' => 'Erleben Sie DocuWare in Aktion', - 'teaser' => 'Entdecken Sie interaktive Touren rund um intelligente Dokumentenverarbeitung, Rechnungsfreigabe und - Vertragsmanagement. Sehen Sie live, wie DocuWare Prozesse vereinfacht, Fehler reduziert und Ihr Business - effizienter macht.', - 'buttons' => [ - 'discover_now' => 'Jetzt entdecken', - 'more' => 'Mehr über DocuWare', - ], - ], - ], 'what_we_do' => [ 'title' => 'Was wir tun', 'items' => [ @@ -38,7 +26,39 @@ 'title' => 'KI bei codebar', 'intro' => 'Wie wir künstliche Intelligenz in unserer eigenen Arbeit einsetzen — von den lokalen Modellen, die wir betreiben, bis zur Infrastruktur dahinter.', 'llm_teaser' => 'Die lokalen Open-Source-Sprachmodelle, auf die wir setzen, und die Infrastruktur, die sie betreibt.', - 'more_info' => 'Mehr erfahren', + 'to_models' => 'Zu den Modellen', + 'to_analytics' => 'Zur Nutzungsstatistik', + 'stats' => [ + 'tokens_month' => 'Tokens diesen Monat', + 'requests_month' => 'Anfragen diesen Monat', + 'input' => 'Input', + 'output' => 'Output', + ], + ], + 'ai_llm_analytics' => [ + 'title' => 'LLM-Nutzungsstatistik', + 'intro' => 'Token-Verbrauch und Anfrage-Statistiken unserer lokal betriebenen LLMs — aggregiert pro Monat und Modell.', + 'filter' => [ + 'year_label' => 'Jahr', + 'all_years' => 'Alle Jahre', + 'month_label' => 'Monat', + 'all_months' => 'Alle Monate', + 'model_label' => 'Modell', + 'all_models' => 'Alle Modelle', + 'other_models' => 'Andere', + 'apply' => 'Anzeigen', + ], + 'table' => [ + 'title' => 'Details', + 'period' => 'Zeitraum', + 'prompt_tokens' => 'Input-Tokens', + 'completion_tokens' => 'Output-Tokens', + 'total_tokens' => 'Tokens total', + 'requests' => 'Anfragen', + 'total' => 'Total', + ], + 'empty' => 'Noch keine Nutzungsdaten vorhanden.', + 'back' => 'Zurück zu unseren LLMs', ], 'ai_llm' => [ 'title' => 'Unsere lokalen LLMs im Einsatz', diff --git a/lang/en_CH.json b/lang/en_CH.json index 89c88a2..d2a6caf 100644 --- a/lang/en_CH.json +++ b/lang/en_CH.json @@ -27,7 +27,6 @@ "Imprint Sebastian Burgin role": "Chairman of the Board of Directors and Managing Director", "Imprint Melanie Burgin residence": "from Rothenfluh, resident in Oberwil (BL)", "Imprint Melanie Burgin role": "Member of the Board of Directors", - "Info(at)paperflakes.ch": "Info(at)paperflakes.ch", "info(at)codebar.ch": "info(at)codebar.ch", "Jobs": "Jobs", "Language": "Language", @@ -50,16 +49,13 @@ "Office": "Office", "Open Source": "Open Source", "Partnerships": "Partnerships", - "paperflakes AG": "paperflakes AG", "Phone": "Phone", "Privacy": "Privacy", "Privacy changes body": "We may update this privacy policy from time to time. The current version is always available on this page with the date of the last update.", "Privacy changes heading": "Changes to this policy", "Privacy contact body": "For questions about data protection or to exercise your rights:", - "Privacy contact body paperflakes": "For questions about data protection or to exercise your rights:", "Privacy contact heading": "Contact", "Privacy controller body": "The controller responsible for data processing on this website is codebar Solutions AG.", - "Privacy controller body paperflakes": "The controller responsible for data processing on this website is paperflakes AG.", "Privacy controller heading": "Controller", "Privacy data collected analytics": "Usage analytics via Fathom Analytics (cookieless, anonymized page views; no personal profiles)", "Privacy data collected errors": "Error and performance data sent to Flare for troubleshooting (server-side; sensitive headers are censored)", diff --git a/lang/en_CH/components.php b/lang/en_CH/components.php index 97c7380..7e17f94 100644 --- a/lang/en_CH/components.php +++ b/lang/en_CH/components.php @@ -1,16 +1,6 @@ [ - 'showme' => [ - 'title' => 'Experience DocuWare in Action', - 'teaser' => 'Discover interactive tours focused on intelligent document processing, invoice approval, and contract management. See how DocuWare streamlines processes, reduces errors, and makes your business more efficient.', - 'buttons' => [ - 'discover_now' => 'Discover Now', - 'more' => 'More about DocuWare', - ], - ], - ], 'what_we_do' => [ 'title' => 'What we do', 'items' => [ @@ -36,7 +26,39 @@ 'title' => 'AI at codebar', 'intro' => 'How we use artificial intelligence in our own work — from the local models we run to the infrastructure behind them.', 'llm_teaser' => 'The local open-source language models we rely on and the infrastructure that runs them.', - 'more_info' => 'More information', + 'to_models' => 'View the models', + 'to_analytics' => 'View usage analytics', + 'stats' => [ + 'tokens_month' => 'Tokens this month', + 'requests_month' => 'Requests this month', + 'input' => 'Input', + 'output' => 'Output', + ], + ], + 'ai_llm_analytics' => [ + 'title' => 'LLM usage analytics', + 'intro' => 'Token usage and request statistics of our locally hosted LLMs — aggregated per month and per model.', + 'filter' => [ + 'year_label' => 'Year', + 'all_years' => 'All years', + 'month_label' => 'Month', + 'all_months' => 'All months', + 'model_label' => 'Model', + 'all_models' => 'All models', + 'other_models' => 'Other', + 'apply' => 'Apply', + ], + 'table' => [ + 'title' => 'Details', + 'period' => 'Period', + 'prompt_tokens' => 'Prompt tokens', + 'completion_tokens' => 'Completion tokens', + 'total_tokens' => 'Total tokens', + 'requests' => 'Requests', + 'total' => 'Total', + ], + 'empty' => 'No usage data available yet.', + 'back' => 'Back to our LLMs', ], 'ai_llm' => [ 'title' => 'Our local LLMs in action', diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd4c48e..caf429c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -35,8 +35,8 @@ - - + + diff --git a/public/favicons/paperflakes/apple-touch-icon.png b/public/favicons/paperflakes/apple-touch-icon.png deleted file mode 100644 index b9b4b47..0000000 Binary files a/public/favicons/paperflakes/apple-touch-icon.png and /dev/null differ diff --git a/public/favicons/paperflakes/favicon-96x96.png b/public/favicons/paperflakes/favicon-96x96.png deleted file mode 100644 index ece4b83..0000000 Binary files a/public/favicons/paperflakes/favicon-96x96.png and /dev/null differ diff --git a/public/favicons/paperflakes/favicon.ico b/public/favicons/paperflakes/favicon.ico deleted file mode 100644 index 16c980d..0000000 Binary files a/public/favicons/paperflakes/favicon.ico and /dev/null differ diff --git a/public/favicons/paperflakes/favicon.svg b/public/favicons/paperflakes/favicon.svg deleted file mode 100644 index 0c5b78b..0000000 --- a/public/favicons/paperflakes/favicon.svg +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/public/favicons/paperflakes/site.webmanifest b/public/favicons/paperflakes/site.webmanifest deleted file mode 100644 index 61b50ab..0000000 --- a/public/favicons/paperflakes/site.webmanifest +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "paperflakes AG", - "short_name": "paperflakes", - "icons": [ - { - "src": "web-app-manifest-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "web-app-manifest-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/public/favicons/paperflakes/web-app-manifest-192x192.png b/public/favicons/paperflakes/web-app-manifest-192x192.png deleted file mode 100644 index 3c1e3ba..0000000 Binary files a/public/favicons/paperflakes/web-app-manifest-192x192.png and /dev/null differ diff --git a/public/favicons/paperflakes/web-app-manifest-512x512.png b/public/favicons/paperflakes/web-app-manifest-512x512.png deleted file mode 100644 index c0062d5..0000000 Binary files a/public/favicons/paperflakes/web-app-manifest-512x512.png and /dev/null differ diff --git a/resources/js/app.js b/resources/js/app.js index 5cbb46f..357e79a 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -4,6 +4,50 @@ import Alpine from '@alpinejs/csp' window.Alpine = Alpine +Alpine.data('autoSubmit', () => ({ + submit() { + this.$root.submit() + }, +})) + +Alpine.data('combobox', () => ({ + isOpen: false, + hasValue: false, + + init() { + this.hasValue = this.$refs.input.value !== '' + }, + + open() { + this.isOpen = true + this.filter() + }, + + close() { + this.isOpen = false + }, + + filter() { + const query = this.$refs.input.value.toLowerCase() + this.hasValue = query !== '' + Array.from(this.$refs.list.children).forEach((item) => { + item.style.display = item.dataset.value.toLowerCase().includes(query) ? '' : 'none' + }) + }, + + select(event) { + this.$refs.input.value = event.currentTarget.dataset.value + this.isOpen = false + this.$root.closest('form').submit() + }, + + clear() { + this.$refs.input.value = '' + this.isOpen = false + this.$root.closest('form').submit() + }, +})) + Alpine.data('navigation', () => ({ open: false, toggle() { diff --git a/resources/views/app/ai/index.blade.php b/resources/views/app/ai/index.blade.php index eea9e36..4276176 100644 --- a/resources/views/app/ai/index.blade.php +++ b/resources/views/app/ai/index.blade.php @@ -5,7 +5,27 @@

{{ __('components.ai.llm_teaser') }}

- + + @if ($llmSummary['requests'] > 0) +
+ + +
+ @endif + +
+ + @if ($llmSummary['requests'] > 0) + + @endif +
diff --git a/resources/views/app/ai/llm/analytics.blade.php b/resources/views/app/ai/llm/analytics.blade.php new file mode 100644 index 0000000..d24c7b4 --- /dev/null +++ b/resources/views/app/ai/llm/analytics.blade.php @@ -0,0 +1,112 @@ + + +

{{ __('components.ai_llm_analytics.intro') }}

+ + +
+ + + + + + + + +
+ + @if ($totalSummary['requests'] > 0) + +
+ + + +
+
+ @endif + + @if ($periods->isEmpty()) + +

{{ __('components.ai_llm_analytics.empty') }}

+
+ @else + + + +
+ + + + + + + + + + + + @foreach ($periods as $row) + + + + + + + + @endforeach + + + + + + + + + + +
{{ __('components.ai_llm_analytics.table.period') }}{{ __('components.ai_llm_analytics.table.total_tokens') }}{{ __('components.ai_llm_analytics.table.requests') }}
{{ \App\Helpers\Facades\HelperDate::monthLabel($row['label']) }}{{ \App\Helpers\Facades\HelperNumber::format($row['total_tokens'], 0) }}{{ \App\Helpers\Facades\HelperNumber::format($row['requests'], 0) }}
{{ __('components.ai_llm_analytics.table.total') }}{{ \App\Helpers\Facades\HelperNumber::format($grandTotal['total_tokens'], 0) }}{{ \App\Helpers\Facades\HelperNumber::format($grandTotal['requests'], 0) }}
+
+
+
+ + @if ($periods->hasPages()) + + {{ $periods->onEachSide(1)->links() }} + + @endif + @endif + + + + +
diff --git a/resources/views/app/contact/_partials/_codebar.blade.php b/resources/views/app/contact/_partials/_codebar.blade.php deleted file mode 100644 index 68645b2..0000000 --- a/resources/views/app/contact/_partials/_codebar.blade.php +++ /dev/null @@ -1,55 +0,0 @@ - - - -
- - -
-
- - -
-
- - - -
-
- -
-

codebar Solutions AG

-

Hauptstrasse 91

-

CH-4455 Zunzgen

-
- - - - - -
-
- -
-

codebar Solutions AG

-

Langegasse 39

-

CH-4104 Oberwil

-
- - - - - -
-
-
\ No newline at end of file diff --git a/resources/views/app/contact/_partials/_paperflakes.blade.php b/resources/views/app/contact/_partials/_paperflakes.blade.php deleted file mode 100644 index db95627..0000000 --- a/resources/views/app/contact/_partials/_paperflakes.blade.php +++ /dev/null @@ -1,52 +0,0 @@ - - - -
- - -
-
- - -
-
- - - -
-
-
-

paperflakes AG

-

{{ __('Headquarter') }}

-

Haupstrasse 91

-

CH-4455 Zunzgen

-
- - - - - -
-
-
-

paperflakes AG

-

{{__('Branch office')}}

-

Langegasse 39

-

CH-4104 Oberwil

-
- - - - - - -
-
-
\ No newline at end of file diff --git a/resources/views/app/contact/index.blade.php b/resources/views/app/contact/index.blade.php index c357578..9dff229 100644 --- a/resources/views/app/contact/index.blade.php +++ b/resources/views/app/contact/index.blade.php @@ -1,5 +1,57 @@ - @if(filled($configuration?->key)) - @include("app.contact._partials.{$configuration->key}") - @endif - \ No newline at end of file + + + +
+ + +
+
+ + +
+
+ + + +
+
+ +
+

codebar Solutions AG

+

Hauptstrasse 91

+

CH-4455 Zunzgen

+
+ + + + + +
+
+ +
+

codebar Solutions AG

+

Langegasse 39

+

CH-4104 Oberwil

+
+ + + + + +
+
+
+ diff --git a/resources/views/app/legal/imprint/_partials/_codebar.blade.php b/resources/views/app/legal/imprint/_partials/_codebar.blade.php deleted file mode 100644 index b285022..0000000 --- a/resources/views/app/legal/imprint/_partials/_codebar.blade.php +++ /dev/null @@ -1,41 +0,0 @@ - - - - -
-

codebar Solutions AG

-

{{ __('Legal form AG') }}

-

CHE-257.955.682

-
- - - - - -
- - -
- - -
-
- - -
-
- - - - -
    -
  • Sebastian Bürgin
  • -
  • Melanie Sabrina Bürgin
  • -
-
-
- - - -

{{ __('Imprint disclaimer') }}

-
diff --git a/resources/views/app/legal/imprint/_partials/_paperflakes.blade.php b/resources/views/app/legal/imprint/_partials/_paperflakes.blade.php deleted file mode 100644 index bd005f0..0000000 --- a/resources/views/app/legal/imprint/_partials/_paperflakes.blade.php +++ /dev/null @@ -1,36 +0,0 @@ - - - - -
-

paperflakes AG

-

{{ __('Legal form AG') }}

-

CHE-432.585.498

-
- - - - - -
- - -
- - -
-
- - -
-
- - - -

{{ __('Imprint representatives placeholder') }}

-
- - - -

{{ __('Imprint disclaimer') }}

-
diff --git a/resources/views/app/legal/imprint/index.blade.php b/resources/views/app/legal/imprint/index.blade.php index 896d214..486c753 100644 --- a/resources/views/app/legal/imprint/index.blade.php +++ b/resources/views/app/legal/imprint/index.blade.php @@ -1,6 +1,43 @@ - @if(filled($configuration?->key)) - @include("app.legal.imprint._partials.{$configuration->key}") - @endif - + + + + +
+

codebar Solutions AG

+

{{ __('Legal form AG') }}

+

CHE-257.955.682

+
+ + + + + +
+ +
+ + +
+
+ + +
+
+ + + + +
    +
  • Sebastian Bürgin
  • +
  • Melanie Sabrina Bürgin
  • +
+
+
+ + + +

{{ __('Imprint disclaimer') }}

+
+ diff --git a/resources/views/app/legal/privacy/_partials/_codebar.blade.php b/resources/views/app/legal/privacy/_partials/_codebar.blade.php deleted file mode 100644 index 93ad04c..0000000 --- a/resources/views/app/legal/privacy/_partials/_codebar.blade.php +++ /dev/null @@ -1,80 +0,0 @@ - - -

{{ __('Last updated at: :date', ['date' => '2026-06-30']) }}

- - - - -

{{ __('Privacy controller body') }}

-
-
- - - - -

{{ __('Privacy scope body') }}

-
-
- - - - -

{{ __('Privacy data collected intro') }}

-
    -
  • {{ __('Privacy data collected logs') }}
  • -
  • {{ __('Privacy data collected session') }}
  • -
  • {{ __('Privacy data collected analytics') }}
  • -
  • {{ __('Privacy data collected errors') }}
  • -
-
-
- - - - -

{{ __('Privacy purpose body') }}

-
-
- - - - -
    -
  • {{ __('Privacy retention session') }}
  • -
  • {{ __('Privacy retention logs') }}
  • -
  • {{ __('Privacy retention analytics') }}
  • -
  • {{ __('Privacy retention errors') }}
  • -
-
-
- - - - -

{{ __('Privacy rights body') }}

-
-
- - - - -

{{ __('Privacy security body') }}

-
-
- - - - -

{{ __('Privacy changes body') }}

-
-
- - - - -

- {{ __('Privacy contact body') }} - -

-
-
diff --git a/resources/views/app/legal/privacy/_partials/_paperflakes.blade.php b/resources/views/app/legal/privacy/_partials/_paperflakes.blade.php deleted file mode 100644 index 9ac9679..0000000 --- a/resources/views/app/legal/privacy/_partials/_paperflakes.blade.php +++ /dev/null @@ -1,80 +0,0 @@ - - -

{{ __('Last updated at: :date', ['date' => '2026-06-30']) }}

- - - - -

{{ __('Privacy controller body paperflakes') }}

-
-
- - - - -

{{ __('Privacy scope body') }}

-
-
- - - - -

{{ __('Privacy data collected intro') }}

-
    -
  • {{ __('Privacy data collected logs') }}
  • -
  • {{ __('Privacy data collected session') }}
  • -
  • {{ __('Privacy data collected analytics') }}
  • -
  • {{ __('Privacy data collected errors') }}
  • -
-
-
- - - - -

{{ __('Privacy purpose body') }}

-
-
- - - - -
    -
  • {{ __('Privacy retention session') }}
  • -
  • {{ __('Privacy retention logs') }}
  • -
  • {{ __('Privacy retention analytics') }}
  • -
  • {{ __('Privacy retention errors') }}
  • -
-
-
- - - - -

{{ __('Privacy rights body') }}

-
-
- - - - -

{{ __('Privacy security body') }}

-
-
- - - - -

{{ __('Privacy changes body') }}

-
-
- - - - -

- {{ __('Privacy contact body paperflakes') }} - -

-
-
diff --git a/resources/views/app/legal/privacy/index.blade.php b/resources/views/app/legal/privacy/index.blade.php index bc2c3b0..0e15f73 100644 --- a/resources/views/app/legal/privacy/index.blade.php +++ b/resources/views/app/legal/privacy/index.blade.php @@ -1,5 +1,82 @@ - @if(filled($configuration?->key)) - @include("app.legal.privacy._partials.{$configuration->key}") - @endif - \ No newline at end of file + + +

{{ __('Last updated at: :date', ['date' => '2026-06-30']) }}

+ + + + +

{{ __('Privacy controller body') }}

+
+
+ + + + +

{{ __('Privacy scope body') }}

+
+
+ + + + +

{{ __('Privacy data collected intro') }}

+
    +
  • {{ __('Privacy data collected logs') }}
  • +
  • {{ __('Privacy data collected session') }}
  • +
  • {{ __('Privacy data collected analytics') }}
  • +
  • {{ __('Privacy data collected errors') }}
  • +
+
+
+ + + + +

{{ __('Privacy purpose body') }}

+
+
+ + + + +
    +
  • {{ __('Privacy retention session') }}
  • +
  • {{ __('Privacy retention logs') }}
  • +
  • {{ __('Privacy retention analytics') }}
  • +
  • {{ __('Privacy retention errors') }}
  • +
+
+
+ + + + +

{{ __('Privacy rights body') }}

+
+
+ + + + +

{{ __('Privacy security body') }}

+
+
+ + + + +

{{ __('Privacy changes body') }}

+
+
+ + + + +

+ {{ __('Privacy contact body') }} + +

+
+
+ diff --git a/resources/views/app/legal/terms/_partials/_codebar.blade.php b/resources/views/app/legal/terms/_partials/_codebar.blade.php deleted file mode 100644 index 20dee71..0000000 --- a/resources/views/app/legal/terms/_partials/_codebar.blade.php +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/views/app/legal/terms/_partials/_paperflakes.blade.php b/resources/views/app/legal/terms/_partials/_paperflakes.blade.php deleted file mode 100644 index 20dee71..0000000 --- a/resources/views/app/legal/terms/_partials/_paperflakes.blade.php +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/views/app/legal/terms/index.blade.php b/resources/views/app/legal/terms/index.blade.php index 8a5b8f5..6962672 100644 --- a/resources/views/app/legal/terms/index.blade.php +++ b/resources/views/app/legal/terms/index.blade.php @@ -1,5 +1,3 @@ - @if(filled($configuration?->key)) - @include("app.legal.terms._partials.{$configuration->key}") - @endif - \ No newline at end of file + + diff --git a/resources/views/app/products/show.blade.php b/resources/views/app/products/show.blade.php index a0bb1cb..ce207d0 100644 --- a/resources/views/app/products/show.blade.php +++ b/resources/views/app/products/show.blade.php @@ -7,8 +7,4 @@ - @if(collect(['DMS/ECM', 'DocuWare'])->diff($tags)->isEmpty()) - - @endif - \ No newline at end of file diff --git a/resources/views/app/services/show.blade.php b/resources/views/app/services/show.blade.php index a0bb1cb..ce207d0 100644 --- a/resources/views/app/services/show.blade.php +++ b/resources/views/app/services/show.blade.php @@ -7,8 +7,4 @@ - @if(collect(['DMS/ECM', 'DocuWare'])->diff($tags)->isEmpty()) - - @endif - \ No newline at end of file diff --git a/resources/views/app/start/index.blade.php b/resources/views/app/start/index.blade.php index 45a282b..03624af 100644 --- a/resources/views/app/start/index.blade.php +++ b/resources/views/app/start/index.blade.php @@ -13,21 +13,5 @@ - @if($configuration?->section_news) - - - - @foreach($news as $entry) - - @endforeach - - - @endif - - diff --git a/resources/views/components/docuware-showme.blade.php b/resources/views/components/docuware-showme.blade.php deleted file mode 100644 index f0713d7..0000000 --- a/resources/views/components/docuware-showme.blade.php +++ /dev/null @@ -1,47 +0,0 @@ -@use(App\Enums\LocaleEnum;use Illuminate\Support\Facades\Config;use Illuminate\Support\Str) - -@php - $display = Config::get('seeder.seeder.paperflakes'); - - $showme_url = match (app()->getLocale()) { - LocaleEnum::EN->value => 'https://showme.docuware.com/en-gb/interactive-tours', - default => 'https://showme.docuware.com/de/interactive-tours', - }; - - $product_url = match (app()->getLocale()) { - LocaleEnum::EN->value => 'https://www.paperflakes.ch/services/en_CH/dms-ecm-docuware', - default => 'https://www.paperflakes.ch/dienstleistungen/de_CH/dms-ecm-docuware', - }; -@endphp - -@if ($display) -
- -
-
-
-
- -

- {{ __('components.docuware.showme.teaser') }} -

-
- - {{ __('components.docuware.showme.buttons.discover_now') }} - - - - - - -
-
-
-
-
-@endif diff --git a/resources/views/components/intro.blade.php b/resources/views/components/intro.blade.php index 86dd4b1..7dac382 100644 --- a/resources/views/components/intro.blade.php +++ b/resources/views/components/intro.blade.php @@ -1,9 +1,10 @@ -@use(App\Helpers\HelperMarkdown;use Illuminate\Support\Arr) +@use(App\Helpers\HelperMarkdown;use Illuminate\Support\Str) @php $locale = app()->getLocale(); + $localeCode = Str::before($locale, '_'); - $markdownContent = Arr::get($configuration?->component_intro, $locale); + $markdownContent = file_get_contents(database_path("files/intro/codebar_intro_{$localeCode}.md")); $htmlContent = $markdownContent ? app(HelperMarkdown::class)->formatMarkdown($markdownContent) : ''; $htmlContent = preg_replace('/

/', '

', $htmlContent); $htmlContent = preg_replace('/

/', '

', $htmlContent); diff --git a/resources/views/components/llm-analytics/filter-combobox.blade.php b/resources/views/components/llm-analytics/filter-combobox.blade.php new file mode 100644 index 0000000..b2775dd --- /dev/null +++ b/resources/views/components/llm-analytics/filter-combobox.blade.php @@ -0,0 +1,33 @@ +@props(['name', 'label', 'value' => null, 'placeholder' => null, 'options' => []]) + +

+ +
+
    + @foreach ($options as $option) +
  • {{ $option }}
  • + @endforeach +
+
+
diff --git a/resources/views/components/llm-analytics/stat-card.blade.php b/resources/views/components/llm-analytics/stat-card.blade.php new file mode 100644 index 0000000..d787289 --- /dev/null +++ b/resources/views/components/llm-analytics/stat-card.blade.php @@ -0,0 +1,20 @@ +@props(['label', 'value', 'input' => null, 'output' => null]) + +
+
+
{{ $value }}
+
{{ $label }}
+
+ @if ($input !== null || $output !== null) +
+
+ {{ __('components.ai.stats.input') }} + {{ $input }} +
+
+ {{ __('components.ai.stats.output') }} + {{ $output }} +
+
+ @endif +
diff --git a/resources/views/components/what-we-do.blade.php b/resources/views/components/what-we-do.blade.php index cf6b230..382801a 100644 --- a/resources/views/components/what-we-do.blade.php +++ b/resources/views/components/what-we-do.blade.php @@ -2,16 +2,14 @@ $items = ['concept', 'development', 'dms']; @endphp -@if ($configuration?->key === '_codebar') - - -
- @foreach ($items as $key) -
- -

{{ __('components.what_we_do.items.' . $key . '.description') }}

-
- @endforeach -
-
-@endif + + +
+ @foreach ($items as $key) +
+ +

{{ __('components.what_we_do.items.' . $key . '.description') }}

+
+ @endforeach +
+
diff --git a/resources/views/layouts/_logos/_paperflakes.blade.php b/resources/views/layouts/_logos/_paperflakes.blade.php deleted file mode 100644 index 4a3f9cd..0000000 --- a/resources/views/layouts/_logos/_paperflakes.blade.php +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/resources/views/layouts/_partials/_favicons.blade.php b/resources/views/layouts/_partials/_favicons.blade.php index b478894..59094dc 100644 --- a/resources/views/layouts/_partials/_favicons.blade.php +++ b/resources/views/layouts/_partials/_favicons.blade.php @@ -1,15 +1,7 @@ @props(['manifest' => asset('manifest.json'), 'path' => asset('favicons'), 'color' => '#ffffff']) @php - $prefix = match ($configuration?->key) { - '_paperflakes' => 'paperflakes', - '_codebar' => 'codebar', - default => filled($configuration?->key) - ? ltrim((string) $configuration->key, '_') - : 'codebar', - }; - - $faviconPath = rtrim($path, '/')."/{$prefix}"; + $faviconPath = rtrim($path, '/').'/codebar'; @endphp diff --git a/resources/views/layouts/_partials/_footer.blade.php b/resources/views/layouts/_partials/_footer.blade.php index 9aad251..8b81c13 100644 --- a/resources/views/layouts/_partials/_footer.blade.php +++ b/resources/views/layouts/_partials/_footer.blade.php @@ -1,44 +1,6 @@
- @if($configuration?->section_services || $configuration?->section_products) - - @endif -

{{ __('Legal') }}

@@ -68,10 +30,8 @@ classAttributes="text-lg"/> @include('layouts._partials._footer.labels')
- @if(filled($configuration?->company)) -
- © {{ date('Y') }} {{ $configuration?->company }} -
- @endif +
+ © {{ date('Y') }} codebar Solutions AG +
diff --git a/resources/views/layouts/_partials/_navigation.blade.php b/resources/views/layouts/_partials/_navigation.blade.php index e6e584f..2765bce 100644 --- a/resources/views/layouts/_partials/_navigation.blade.php +++ b/resources/views/layouts/_partials/_navigation.blade.php @@ -1,11 +1,9 @@