Prechádzať zdrojové kódy

Traefik и мелкие изменения

Artem Kastrov 2 mesiacov pred
rodič
commit
6df76ed24f

+ 2 - 0
.env.example

@@ -98,3 +98,5 @@ SENTRY_TRACES_SAMPLE_RATE=0.2
 SCOUT_DRIVER=meilisearch
 MEILISEARCH_HOST=http://meilisearch:7700
 MEILISEARCH_KEY=secret
+
+INFERENCE_HOST=

+ 24 - 0
app/Http/Controllers/ChatController.php

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
 
 use App\Models\Chat;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Http;
 
 class ChatController extends Controller
 {
@@ -24,6 +25,29 @@ class ChatController extends Controller
 
         //TODO: Send To LLM
 
+        $response = Http::inference()->post('/chat', [
+            'prompt' => $request->input('message')
+        ]);
+
+        $result = $response->json();
+        $chat->messages()->create([
+            'text' => $result['message']['content'],
+            'thinking' => $result['message']['thinking'],
+            'from' => $result['message']['role']
+        ]);
+
+        if (!$chat->title) {
+            $response = Http::inference()->post('/generate', [
+                'prompt' => 'На основе первого сообщения пользователя предложи короткое название для чата длиной 2-4 слова.' . ' ' .
+                    'Название должно быть ёмким, отражать суть сообщения, без лишних слов.' . ' ' .
+                    'Ответ дай только в виде текста названия, без пояснений и кавычек.' . ' ' .
+                    'Первое сообщение: ' . $request->input('message')
+            ]);
+
+            $result = $response->json();
+            $chat->update(['title' => $result['response']]);
+        }
+
         return redirect()->route('chats.view', ['chat' => $chat->id]);
     }
 

+ 1 - 1
app/Http/Resources/MessageResource.php

@@ -15,7 +15,7 @@ class MessageResource extends JsonResource
     public function toArray(Request $request): array
     {
         return array_merge(parent::toArray($request), [
-            'text' => str($this->text)->replace("\n", '<br>')
+            //
         ]);
     }
 }

+ 1 - 1
app/Models/Chat.php

@@ -10,7 +10,7 @@ class Chat extends Model
 {
     use HasUuids;
 
-    protected $fillable = ['text'];
+    protected $fillable = ['title', 'text'];
 
     public function messages(): HasMany
     {

+ 1 - 2
app/Models/Message.php

@@ -2,13 +2,12 @@
 
 namespace App\Models;
 
-use Illuminate\Database\Eloquent\Concerns\HasUuids;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
 class Message extends Model
 {
-    protected $fillable = ['text'];
+    protected $fillable = ['text', 'thinking', 'from'];
 
     public function chat(): BelongsTo
     {

+ 3 - 0
app/Providers/AppServiceProvider.php

@@ -7,6 +7,7 @@ use Carbon\CarbonImmutable;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Support\Facades\Http;
 use Illuminate\Support\ServiceProvider;
 
 class AppServiceProvider extends ServiceProvider
@@ -43,5 +44,7 @@ class AppServiceProvider extends ServiceProvider
         ]);
 
         JsonResource::withoutWrapping();
+
+        Http::macro('inference', fn() => Http::baseUrl(config('services.inference.host'))->timeout(120));
     }
 }

+ 1 - 1
config/octane.php

@@ -219,6 +219,6 @@ return [
     |
     */
 
-    'max_execution_time' => 30,
+    'max_execution_time' => 180,
 
 ];

+ 4 - 0
config/services.php

@@ -35,4 +35,8 @@ return [
         ],
     ],
 
+    'inference' => [
+        'host' => env('INFERENCE_HOST'),
+    ]
+
 ];

+ 5 - 0
database/migrations/2025_11_09_131811_create_messages_table.php

@@ -19,6 +19,11 @@ return new class extends Migration {
             $table->longText('text')->nullable();
             $table->string('from')->default('user');
 
+            $table->integer('tokens_in')->default(0);
+            $table->integer('tokens_out')->default(0);
+
+            $table->longText('thinking')->nullable();
+
             $table->timestamps();
         });
     }

+ 15 - 0
docker-compose.traefik.yml

@@ -6,6 +6,10 @@ services:
       - "traefik.http.routers.application-http.rule=Host(`laravel.localhost`)"
       - "traefik.http.routers.application-http.service=application"
       - "traefik.http.services.application.loadbalancer.server.port=9000"
+      - "traefik.docker.network=proxy"
+    networks:
+      - default
+      - proxy
 
   vite:
     labels:
@@ -33,6 +37,7 @@ services:
       - "--entrypoints.vite.address=:5173"
       - "--entrypoints.traefik.address=:8888"
       - "--providers.file.watch=true"
+      - "--providers.file.directory=/configuration/"
       - "--api.dashboard=true"
     ports:
       - "80:80"
@@ -40,3 +45,13 @@ services:
       - "8888:8888"
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock
+      - .docker/traefik/configuration:/configuration/
+      - .docker/traefik/certs:/etc/certs:ro
+    networks:
+      - default
+      - proxy
+
+networks:
+  proxy:
+    name: proxy
+    driver: bridge

+ 56 - 0
package-lock.json

@@ -6,6 +6,7 @@
         "": {
             "dependencies": {
                 "@inertiajs/vue3": "^2.2.15",
+                "@tailwindcss/typography": "github:tailwindcss/typography",
                 "@vitejs/plugin-vue": "^6.0.1",
                 "@vueuse/core": "^14.0.0",
                 "chokidar": "^4.0.3",
@@ -13,6 +14,7 @@
                 "clsx": "^2.1.1",
                 "laravel-vite-plugin": "^2.0.0",
                 "lucide-vue-next": "^0.553.0",
+                "marked": "^17.0.0",
                 "reka-ui": "^2.6.0",
                 "tailwind-merge": "^3.3.1",
                 "tailwindcss": "^4.0.0",
@@ -1201,6 +1203,17 @@
                 "node": ">= 10"
             }
         },
+        "node_modules/@tailwindcss/typography": {
+            "version": "0.5.19",
+            "resolved": "git+ssh://git@github.com/tailwindcss/typography.git#abf85cc6e1b4f9b914b0f66453e5a97a9899a15c",
+            "license": "MIT",
+            "dependencies": {
+                "postcss-selector-parser": "6.0.10"
+            },
+            "peerDependencies": {
+                "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
+            }
+        },
         "node_modules/@tailwindcss/vite": {
             "version": "4.1.17",
             "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz",
@@ -1645,6 +1658,18 @@
                 "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
             }
         },
+        "node_modules/cssesc": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+            "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+            "license": "MIT",
+            "bin": {
+                "cssesc": "bin/cssesc"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/csstype": {
             "version": "3.1.3",
             "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -2329,6 +2354,18 @@
                 "@jridgewell/sourcemap-codec": "^1.5.5"
             }
         },
+        "node_modules/marked": {
+            "version": "17.0.0",
+            "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz",
+            "integrity": "sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==",
+            "license": "MIT",
+            "bin": {
+                "marked": "bin/marked.js"
+            },
+            "engines": {
+                "node": ">= 20"
+            }
+        },
         "node_modules/math-intrinsics": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2441,6 +2478,19 @@
                 "node": "^10 || ^12 || >=14"
             }
         },
+        "node_modules/postcss-selector-parser": {
+            "version": "6.0.10",
+            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+            "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+            "license": "MIT",
+            "dependencies": {
+                "cssesc": "^3.0.0",
+                "util-deprecate": "^1.0.2"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/proxy-from-env": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -2816,6 +2866,12 @@
                 "node": ">=14.17"
             }
         },
+        "node_modules/util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+            "license": "MIT"
+        },
         "node_modules/vite": {
             "version": "7.2.2",
             "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",

+ 2 - 0
package.json

@@ -13,6 +13,7 @@
     },
     "dependencies": {
         "@inertiajs/vue3": "^2.2.15",
+        "@tailwindcss/typography": "github:tailwindcss/typography",
         "@vitejs/plugin-vue": "^6.0.1",
         "@vueuse/core": "^14.0.0",
         "chokidar": "^4.0.3",
@@ -20,6 +21,7 @@
         "clsx": "^2.1.1",
         "laravel-vite-plugin": "^2.0.0",
         "lucide-vue-next": "^0.553.0",
+        "marked": "^17.0.0",
         "reka-ui": "^2.6.0",
         "tailwind-merge": "^3.3.1",
         "tailwindcss": "^4.0.0",

+ 2 - 0
resources/css/app.css

@@ -13,6 +13,8 @@
     'Segoe UI Symbol', 'Noto Color Emoji';
 }
 
+@plugin "@tailwindcss/typography";
+
 :root {
     --radius: 1rem;
     --background: oklch(1 0 0);

+ 1 - 1
resources/js/Components/AppSidebar.vue

@@ -74,7 +74,7 @@ const items = [{title: "Нет чатов", url: "#"}];
                                         </SidebarMenuAction>
                                     </DropdownMenuTrigger>
                                     <DropdownMenuContent side="right" align="center">
-                                        <DropdownMenuItem class="text-primary cursor-pointer" @click="() => router.delete(route('chats.destroy', item.id))">
+                                        <DropdownMenuItem class="text-primary cursor-pointer" @click="() => router.delete(route('chats.destroy', item.id), { preserveScroll: false, preserveState: false })">
                                             <Trash/>
                                             <span>Удалить чат</span>
                                         </DropdownMenuItem>

+ 7 - 6
resources/js/Pages/Chat.vue

@@ -9,6 +9,7 @@ import {
 } from "@/Packages/Shadcn/Components/ui/input-group"
 import {computed, nextTick, watch} from "vue";
 import {useForm} from "@inertiajs/vue3";
+import { parse } from 'marked'
 
 const { chat } = defineProps(['chats', 'chat'])
 const form = useForm({
@@ -17,14 +18,14 @@ const form = useForm({
 })
 
 const submit = function () {
-    if (!form.message) return
+    if (!form.message || form.processing) return
     form.post(route('chats.message'), {
         onSuccess: () => form.reset()
     })
 }
 
 const handleEnter = function (e) {
-    if (e.metaKey || e.ctrlKey) {
+    if (e.metaKey || e.ctrlKey || e.shiftKey) {
         const {selectionStart, selectionEnd, value} = e.target
         form.message = value.slice(0, selectionStart) + '\n' + value.slice(selectionEnd)
         nextTick(() => {
@@ -46,20 +47,20 @@ watch(() => chat?.id, () => form.defaults('uuid', chat?.id ?? null))
                 <div v-for="message in messages">
                     <div class="flex" v-if="message.from === 'user'">
                         <div class="ml-auto border bg-card text-card-foreground p-2 rounded-lg max-w-[75%]"
-                             v-html="message.text"></div>
+                             v-html="message.text.replaceAll('\n', '<br />')"></div>
                     </div>
-                    <div v-else>{{ message.text }}</div>
+                    <div class="prose dark:prose-invert" v-else v-html="parse(message.text)"></div>
                 </div>
                 <!--                <div class=""></div>-->
             </div>
 
-            <div class="my-auto pb-2" :class="{'sticky bottom-0': messages.length !== 0}">
+            <div class="my-auto pb-2 pt-5" :class="{'sticky bottom-0': messages.length !== 0}">
                 <InputGroup class="!bg-card">
                     <InputGroupTextarea placeholder="Ask, Search or Chat..." v-model="form.message"
                                         @keydown.enter.prevent="handleEnter"/>
                     <InputGroupAddon align="block-end">
                         <InputGroupButton class="ml-auto cursor-pointer" variant="default"
-                                          :disabled="!form.message" :loading="true"
+                                          :disabled="!form.message || form.processing" :loading="true"
                                           @click="() => submit()">
                             <template v-if="!form.processing">
                                 <ArrowUpIcon class="size-5"/>