- first commit

This commit is contained in:
Roland 2026-05-14 12:31:35 +02:00
commit 6abb069dd0
104 changed files with 62867 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/bin/*
/obj/*

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,62 @@
{
"Version": 1,
"WorkspaceRootPath": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}",
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}"
},
{
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 0,
"Children": [
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "AuthorBuddy.Web.csproj",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeToolTip": "AuthorBuddy.Web.csproj",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|",
"WhenOpened": "2026-05-14T10:14:30.128Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "appsettings.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeDocumentMoniker": "appsettings.json",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeToolTip": "appsettings.json",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
"WhenOpened": "2026-05-14T10:14:25.605Z",
"EditorCaption": ""
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
}
]
}
]
}
]
}

View file

@ -0,0 +1,62 @@
{
"Version": 1,
"WorkspaceRootPath": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}",
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}"
},
{
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "AuthorBuddy.Web.csproj",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeToolTip": "AuthorBuddy.Web.csproj",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|",
"WhenOpened": "2026-05-14T10:14:30.128Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "appsettings.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeDocumentMoniker": "appsettings.json",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeToolTip": "appsettings.json",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
"WhenOpened": "2026-05-14T10:14:25.605Z",
"EditorCaption": ""
}
]
}
]
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
.vs/ProjectSettings.json Normal file
View file

@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

View file

@ -0,0 +1,7 @@
{
"ExpandedNodes": [
""
],
"SelectedNode": "\\AuthorBuddy.Web.csproj",
"PreviewInSolutionExplorer": false
}

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

22
AuthorBuddy.Web.csproj Normal file
View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
<PackageReference Include="Ollama" Version="1.15.0" />
</ItemGroup>
<ItemGroup>
<None Update="vec0.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

23
Components/App.razor Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link id="theme-css" rel="stylesheet" href="@Assets["css/themes/light.css"]" />
<link rel="stylesheet" href="@Assets["AuthorBuddy.Web.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

View file

@ -0,0 +1,165 @@
@inherits LayoutComponentBase
@inject IProjectService ProjectService
<div class="page">
<div class="main-content">
<aside class="sidebar">
<div class="nav-section">
<button class="nav-section-header" @onclick='() => ToggleSection("file")'>
<span class="nav-icon">@(activeSection == "file" ? "▼" : "▶")</span>
Datei
</button>
@if (activeSection == "file")
{
<div class="nav-section-body">
<button class="nav-item" @onclick="CreateNewProject">Neues Projekt</button>
<button class="nav-item" @onclick="OpenProject">Öffnen</button>
<div class="nav-separator"></div>
<button class="nav-item" @onclick="ExitApp">Beenden</button>
</div>
}
</div>
<div class="nav-section">
<button class="nav-section-header" @onclick='() => ToggleSection("view")'>
<span class="nav-icon">@(activeSection == "view" ? "▼" : "▶")</span>
Ansicht
</button>
@if (activeSection == "view")
{
<div class="nav-section-body">
<button class="nav-item" @onclick="OpenEditor">Editor öffnen</button>
</div>
}
</div>
<div class="nav-section">
<button class="nav-section-header" @onclick='() => ToggleSection("extras")'>
<span class="nav-icon">@(activeSection == "extras" ? "▼" : "▶")</span>
Extras
</button>
@if (activeSection == "extras")
{
<div class="nav-section-body">
<button class="nav-item" @onclick='() => NavigateTo("/settings")'>Einstellungen</button>
<button class="nav-item" @onclick='() => NavigateTo("/models")'>Modelle verwalten</button>
</div>
}
</div>
<div class="nav-section">
<button class="nav-section-header" @onclick='() => ToggleSection("projects")'>
<span class="nav-icon">@(activeSection == "projects" ? "▼" : "▶")</span>
Projekte
</button>
@if (activeSection == "projects")
{
<div class="nav-section-body">
@if (projects == null)
{
<span class="nav-hint">Lade Projekte...</span>
}
else if (projects.Count == 0)
{
<span class="nav-hint">Keine Projekte vorhanden.</span>
}
else
{
@foreach (var p in projects)
{
<button class="nav-item" @onclick='() => SelectProject(p)'>@p.Name</button>
}
}
</div>
}
</div>
</aside>
<main class="detail-panel">
@Body
</main>
</div>
<footer class="status-bar">
<span class="status-dot connected"></span>
<span class="status-text">@statusText</span>
@if (isBusy)
{
<progress value="@operationProgress" max="100"></progress>
}
</footer>
</div>
@code {
private string? activeSection;
private List<Project>? projects;
private string statusText = "Bereit";
private bool isBusy;
private double operationProgress;
[Inject]
private NavigationManager Navigation { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await LoadProjects();
}
private async Task LoadProjects()
{
try
{
projects = await ProjectService.GetAllProjectsAsync();
}
catch
{
projects = new();
}
}
private void ToggleSection(string section)
{
activeSection = activeSection == section ? null : section;
}
private void ExitApp()
{
activeSection = null;
Navigation.NavigateTo("/");
}
private void CreateNewProject()
{
activeSection = null;
Navigation.NavigateTo("/editor?action=newproject");
}
private void OpenProject()
{
activeSection = null;
Navigation.NavigateTo("/editor?action=open");
}
private void OpenEditor()
{
activeSection = null;
Navigation.NavigateTo("/editor");
}
private void NavigateTo(string path)
{
activeSection = null;
Navigation.NavigateTo(path);
}
private void SelectProject(Project project)
{
activeSection = null;
Navigation.NavigateTo($"/editor?projectId={project.Id}");
}
public void SetStatus(string text, bool busy = false, double progress = 0)
{
statusText = text;
isBusy = busy;
operationProgress = progress;
StateHasChanged();
}
}

View file

@ -0,0 +1,31 @@
.page {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
}
main {
flex: 1;
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View file

@ -0,0 +1 @@
@* NavMenu is integrated into MainLayout *@

View file

@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View file

@ -0,0 +1,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>

View file

@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}

View file

@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}

View file

@ -0,0 +1,123 @@
@page "/editor"
@page "/editor/{action}"
@page "/editor/{action}/{projectId:int}"
@inject IOllamaService OllamaService
@inject IProjectService ProjectService
@inject NavigationManager Navigation
<PageTitle>Editor - Author Buddy</PageTitle>
<div class="editor-container">
<textarea @bind="editorContent" placeholder="Schreibe hier deinen Text..."></textarea>
<div class="editor-toolbar">
<span style="flex: 1; font-size: 12px; color: var(--border-color);">
@statusText
</span>
@if (isChecking)
{
<progress value="@checkProgress" max="100"></progress>
}
<button @onclick="CheckStyle" disabled="@isChecking">
Stil prüfen
</button>
<button @onclick="SpellCheck" disabled="@isChecking">
Rechtschreibung prüfen
</button>
<button @onclick="RagSearch">
Fakten suchen (RAG)
</button>
</div>
</div>
@if (!string.IsNullOrEmpty(analysisResult))
{
<div style="border-top: 2px solid var(--border-color); padding: 12px; max-height: 300px; overflow-y: auto;">
<h4 style="margin-bottom: 8px;">Ergebnis:</h4>
<div style="white-space: pre-wrap; font-size: 13px; line-height: 1.5;">@analysisResult</div>
</div>
}
@code {
[Parameter]
public string? Action { get; set; }
[Parameter]
public int? ProjectId { get; set; }
private string editorContent = string.Empty;
private string statusText = "Bereit";
private bool isChecking;
private double checkProgress;
private string analysisResult = string.Empty;
protected override async Task OnInitializedAsync()
{
if (Action == "newproject")
{
// The native folder dialog isn't available in Blazor.
// In a real app, you'd provide a form to enter project name and path.
statusText = "Bitte nutze die Einstellungen zum Erstellen eines neuen Projekts.";
}
}
private async Task CheckStyle()
{
if (string.IsNullOrWhiteSpace(editorContent)) return;
isChecking = true;
statusText = "Ollama analysiert Stil...";
checkProgress = 50;
try
{
analysisResult = await OllamaService.StreamStyleCheckAsync(editorContent);
}
catch (Exception ex)
{
analysisResult = $"Fehler: {ex.Message}";
}
finally
{
statusText = "Bereit";
isChecking = false;
checkProgress = 0;
}
}
private async Task SpellCheck()
{
if (string.IsNullOrWhiteSpace(editorContent)) return;
isChecking = true;
statusText = "Ollama prüft Rechtschreibung...";
checkProgress = 50;
try
{
analysisResult = await OllamaService.QuickSpellCheckAsync(editorContent);
}
catch (Exception ex)
{
analysisResult = $"Fehler: {ex.Message}";
}
finally
{
statusText = "Bereit";
isChecking = false;
checkProgress = 0;
}
}
private async Task RagSearch()
{
if (string.IsNullOrWhiteSpace(editorContent)) return;
isChecking = true;
statusText = "Suche verwandte Fakten...";
await Task.Delay(500);
analysisResult = "RAG-Suche: Stelle sicher, dass ein Projekt geladen und indexiert wurde.";
statusText = "Bereit";
isChecking = false;
}
}

View file

@ -0,0 +1,21 @@
@page "/Error"
@rendermode InteractiveServer
<PageTitle>Fehler</PageTitle>
<h1>Ein Fehler ist aufgetreten</h1>
<p>Entschuldigung, es ist ein unerwarteter Fehler aufgetreten.</p>
<a href="/">Zurück zur Startseite</a>
@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
protected override void OnInitialized()
{
if (HttpContext is not null)
{
HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
}
}

View file

@ -0,0 +1,41 @@
@page "/"
@inject IOllamaService OllamaService
@inject IProjectService ProjectService
<PageTitle>Dashboard - Author Buddy</PageTitle>
<div class="tab-bar">
<div class="tab @GetTabClass("rag")" @onclick='() => activeTab = "rag"'>Gefundene Fakten (RAG)</div>
<div class="tab @GetTabClass("style")" @onclick='() => activeTab = "style"'>Stil-Analyse</div>
</div>
<div class="tab-content">
@if (activeTab == "rag")
{
@if (foundFacts.Count == 0)
{
<p style="color: var(--border-color);">Keine Fakten gefunden. Öffne ein Projekt und scanne die Dateien.</p>
}
else
{
@foreach (var fact in foundFacts)
{
<div class="fact-card">@fact</div>
}
}
}
else if (activeTab == "style")
{
<textarea style="width: 100%; min-height: 300px; background: var(--editor-bg); color: var(--text-color); border: 1px solid var(--border-color); padding: 8px; font-family: Consolas;" readonly>@styleSuggestions</textarea>
}
</div>
@code {
private string activeTab = "rag";
private List<string> foundFacts = new();
private string styleSuggestions = string.Empty;
private string GetTabClass(string tab)
{
return activeTab == tab ? "active" : "";
}
}

View file

@ -0,0 +1,143 @@
@page "/models"
@inject IOllamaService OllamaService
@inject NavigationManager Navigation
@using Ollama
<PageTitle>Modelle - Author Buddy</PageTitle>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3>Ollama Modelle verwalten</h3>
<button @onclick="RefreshModels" disabled="@isLoading">Aktualisieren</button>
</div>
@if (isLoading)
{
<p>Lade Modelle...</p>
}
else if (!string.IsNullOrEmpty(errorMessage))
{
<div style="color: red; margin-bottom: 12px;">@errorMessage</div>
}
else
{
<div class="model-list">
@foreach (var model in localModels)
{
<div class="model-card">
<div class="model-name">@model.Model1</div>
<div class="model-details">
Größe: @FormatSize(model.Size) |
Geändert: @model.ModifiedAt?.ToString("g")
</div>
</div>
}
</div>
}
<div style="margin-top: 24px; border-top: 1px solid var(--border-color); padding-top: 16px;">
<h4 style="margin-bottom: 8px;">Neues Modell herunterladen</h4>
<div style="display: flex; gap: 8px; align-items: flex-end;">
<div class="form-group" style="flex: 1; margin-bottom: 0;">
<label for="pullModelName">Modellname:</label>
<input id="pullModelName" @bind="modelToPull" placeholder="z.B. llama3.2" />
</div>
<button @onclick="PullModel" disabled="@(isPulling || string.IsNullOrWhiteSpace(modelToPull))">
Herunterladen
</button>
</div>
@if (isPulling)
{
<div style="margin-top: 8px;">
<div class="progress-bar">
<div class="progress-fill" style="width: @pullPercentDisplay"></div>
</div>
<div style="font-size: 12px; margin-top: 4px;">@pullStatus (@pullPercentDisplay)</div>
</div>
}
</div>
@if (pullResult != null)
{
<div style="margin-top: 12px; padding: 8px; background: var(--card-bg); border: 1px solid var(--accent-color); font-size: 13px;">
@pullResult
</div>
}
@code {
private List<Ollama.Model> localModels = new();
private bool isLoading;
private string? errorMessage;
private string modelToPull = string.Empty;
private bool isPulling;
private double pullProgress;
private string pullStatus = string.Empty;
private string? pullResult;
private string pullPercentDisplay => $"{pullProgress:F0}%";
protected override async Task OnInitializedAsync()
{
await RefreshModels();
}
private async Task RefreshModels()
{
isLoading = true;
errorMessage = null;
try
{
var models = await OllamaService.GetLocalModelsAsync();
localModels = models.ToList();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Laden der Modelle: {ex.Message}";
localModels = new();
}
finally
{
isLoading = false;
}
}
private async Task PullModel()
{
if (string.IsNullOrWhiteSpace(modelToPull)) return;
isPulling = true;
pullProgress = 0;
pullStatus = "Starte Download...";
pullResult = null;
try
{
var progress = new Progress<ModelStatus>(status =>
{
pullProgress = status.Progress;
pullStatus = status.Status;
InvokeAsync(StateHasChanged);
});
await OllamaService.PullModelAsync(modelToPull, progress);
pullResult = $"Modell '{modelToPull}' erfolgreich heruntergeladen.";
modelToPull = string.Empty;
await RefreshModels();
}
catch (Exception ex)
{
pullResult = $"Fehler: {ex.Message}";
}
finally
{
isPulling = false;
}
}
private static string FormatSize(long? bytes)
{
if (!bytes.HasValue) return "Unbekannt";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB";
return $"{bytes / (1024.0 * 1024 * 1024):F1} GB";
}
}

View file

@ -0,0 +1,21 @@
@page "/not-found"
@rendermode InteractiveServer
<PageTitle>Seite nicht gefunden</PageTitle>
<h1>404 - Seite nicht gefunden</h1>
<p>Die angeforderte Seite existiert nicht.</p>
<a href="/">Zurück zur Startseite</a>
@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
protected override void OnInitialized()
{
if (HttpContext is not null)
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
}
}
}

View file

@ -0,0 +1,200 @@
@page "/settings"
@inject IDbContextFactory<AppDbContext> ContextFactory
@inject IOllamaService OllamaService
@inject OllamaSettings OllamaConfig
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@inject IProjectService ProjectService
<PageTitle>Einstellungen - Author Buddy</PageTitle>
<div class="settings-page">
<div class="settings-section">
<h3 class="settings-section-title">Modelle</h3>
<div class="form-group">
<label for="ragModel">RAG-Modell (Faktenabfrage):</label>
<select id="ragModel" @bind="ragModel">
@if (availableModels.Count == 0)
{
<option value="">Keine Modelle gefunden</option>
}
@foreach (var m in availableModels)
{
<option value="@m.Model1">@m.Model1</option>
}
</select>
</div>
<div class="form-group">
<label for="styleModel">Stil-Modell (Lektorat):</label>
<select id="styleModel" @bind="styleModel">
@if (availableModels.Count == 0)
{
<option value="">Keine Modelle gefunden</option>
}
@foreach (var m in availableModels)
{
<option value="@m.Model1">@m.Model1</option>
}
</select>
</div>
<div class="form-group">
<label for="spellingModel">Rechtschreibprüfungs-Modell:</label>
<select id="spellingModel" @bind="spellingModel">
@if (availableModels.Count == 0)
{
<option value="">Keine Modelle gefunden</option>
}
@foreach (var m in availableModels)
{
<option value="@m.Model1">@m.Model1</option>
}
</select>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">Ollama Server</h3>
<div class="form-group">
<label for="ollamaUrl">Server-URL:</label>
<input id="ollamaUrl" @bind="ollamaUrl" placeholder="http://localhost:11434" />
<div class="form-hint">Adresse des Ollama-Servers (z. B. http://192.168.1.100:11434)</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">Design</h3>
<div class="form-group">
<label for="themeSelect">Theme:</label>
<select id="themeSelect" @bind="selectedTheme">
<option value="light">Hell</option>
<option value="dark">Dunkel</option>
<option value="w95">W95 (Retro)</option>
</select>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">Neues Projekt</h3>
<div class="form-group">
<label for="projectName">Projektname:</label>
<input id="projectName" @bind="newProjectName" placeholder="Projektname" />
<div class="form-hint">Der Projektordner wird in &quot;Dokumente\AuthorBuddy\&quot; erstellt.</div>
</div>
<button @onclick="CreateProject" disabled="@(string.IsNullOrWhiteSpace(newProjectName))">Projekt erstellen</button>
</div>
<div class="form-actions">
<button class="primary" @onclick="SaveSettings">Speichern</button>
<button @onclick='() => Navigation.NavigateTo("/")'>Abbrechen</button>
</div>
@if (!string.IsNullOrEmpty(statusMessage))
{
<div class="settings-status">@statusMessage</div>
}
</div>
@code {
private string ragModel = "llama3.2";
private string styleModel = "llama3.2";
private string spellingModel = "phi3:mini";
private string selectedTheme = "light";
private string ollamaUrl = "http://localhost:11434";
private string newProjectName = string.Empty;
private string statusMessage = string.Empty;
private List<Ollama.Model> availableModels = new();
protected override async Task OnInitializedAsync()
{
await using var context = await ContextFactory.CreateDbContextAsync();
ragModel = await GetSettingAsync(context, "ollama_rag_model") ?? "llama3.2";
styleModel = await GetSettingAsync(context, "ollama_style_model") ?? "llama3.2";
spellingModel = await GetSettingAsync(context, "ollama_spelling_model") ?? "phi3:mini";
selectedTheme = await GetSettingAsync(context, "theme") ?? "light";
ollamaUrl = await GetSettingAsync(context, "ollama_url") ?? "http://localhost:11434";
try
{
var models = await OllamaService.GetLocalModelsAsync();
availableModels = models.ToList();
}
catch
{
availableModels = new();
}
}
private static async Task<string?> GetSettingAsync(AppDbContext context, string key)
{
var setting = await context.Settings.FindAsync(key);
return setting?.Value;
}
private static async Task SaveSettingAsync(AppDbContext context, string key, string value)
{
var existing = await context.Settings.FindAsync(key);
if (existing != null)
{
existing.Value = value;
}
else
{
context.Settings.Add(new SettingEntity { Key = key, Value = value });
}
}
private async Task SaveSettings()
{
await using var context = await ContextFactory.CreateDbContextAsync();
await SaveSettingAsync(context, "ollama_rag_model", ragModel);
await SaveSettingAsync(context, "ollama_style_model", styleModel);
await SaveSettingAsync(context, "ollama_spelling_model", spellingModel);
await SaveSettingAsync(context, "theme", selectedTheme);
await SaveSettingAsync(context, "ollama_url", ollamaUrl);
await context.SaveChangesAsync();
OllamaConfig.OllamaUrl = ollamaUrl;
OllamaConfig.ChatModel = ragModel;
OllamaConfig.SpellingModel = spellingModel;
await OllamaService.ReconfigureAsync(ollamaUrl);
statusMessage = "Einstellungen gespeichert.";
await ApplyTheme(selectedTheme);
}
private async Task CreateProject()
{
if (string.IsNullOrWhiteSpace(newProjectName)) return;
try
{
string rootPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"AuthorBuddy",
newProjectName);
await ProjectService.CreateNewProjectAsync(newProjectName, rootPath);
statusMessage = $"Projekt '{newProjectName}' erstellt unter: {rootPath}";
newProjectName = string.Empty;
}
catch (Exception ex)
{
statusMessage = $"Fehler beim Erstellen: {ex.Message}";
}
}
private async Task ApplyTheme(string theme)
{
await JSRuntime.InvokeVoidAsync("eval",
$"document.getElementById('theme-css')?.remove(); " +
$"var link = document.createElement('link'); " +
$"link.id = 'theme-css'; " +
$"link.rel = 'stylesheet'; " +
$"link.href = 'css/themes/{theme}.css'; " +
$"document.head.appendChild(link);");
}
}

6
Components/Routes.razor Normal file
View file

@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

15
Components/_Imports.razor Normal file
View file

@ -0,0 +1,15 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using AuthorBuddy.Web
@using AuthorBuddy.Web.Components
@using AuthorBuddy.Web.Components.Layout
@using AuthorBuddy.Web.Models
@using AuthorBuddy.Web.Services
@using AuthorBuddy.Web.Data
@using Microsoft.EntityFrameworkCore

89
Data/AppDbContext.cs Normal file
View file

@ -0,0 +1,89 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace AuthorBuddy.Web.Data;
public class AppDbContext : DbContext
{
public DbSet<ProjectEntity> Projects => Set<ProjectEntity>();
public DbSet<SettingEntity> Settings => Set<SettingEntity>();
public DbSet<DocumentMetaEntity> DocumentMeta => Set<DocumentMetaEntity>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProjectEntity>(entity =>
{
entity.ToTable("Projects");
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired();
entity.Property(e => e.RootPath).IsRequired();
entity.Property(e => e.LastOpened).HasColumnName("last_opened");
});
modelBuilder.Entity<SettingEntity>(entity =>
{
entity.ToTable("Settings");
entity.HasKey(e => e.Key);
entity.Property(e => e.Key).HasColumnName("key");
entity.Property(e => e.Value).HasColumnName("value").IsRequired();
});
modelBuilder.Entity<DocumentMetaEntity>(entity =>
{
entity.ToTable("DocumentMeta");
entity.HasKey(e => e.Id);
entity.Property(e => e.FilePath).HasColumnName("file_path").IsRequired();
entity.Property(e => e.LastModified).HasColumnName("last_modified");
entity.Property(e => e.FileType).HasColumnName("file_type");
entity.Property(e => e.ProjectId).HasColumnName("project_id");
});
}
public static void InitializeDatabase(string connectionString)
{
using var connection = new SqliteConnection(connectionString);
connection.Open();
connection.EnableExtensions(true);
try
{
connection.LoadExtension("vec0");
using var testCmd = connection.CreateCommand();
testCmd.CommandText = "SELECT vec_version();";
var version = testCmd.ExecuteScalar();
Console.WriteLine($"sqlite-vec Version: {version}");
}
catch (SqliteException ex)
{
Console.WriteLine($"Fehler beim Laden von vec0: {ex.Message}");
}
using var createVecCmd = connection.CreateCommand();
createVecCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
root_path TEXT NOT NULL,
last_opened DATETIME
);
CREATE TABLE IF NOT EXISTS Settings (
key TEXT NOT NULL UNIQUE,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS DocumentMeta (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER,
file_path TEXT UNIQUE,
last_modified DATETIME,
hash TEXT,
file_type TEXT,
FOREIGN KEY(project_id) REFERENCES Projects(id)
);
CREATE VIRTUAL TABLE IF NOT EXISTS vec_documents USING vec0(
embedding float[1024]
);";
createVecCmd.ExecuteNonQuery();
}
}

View file

@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AuthorBuddy.Web.Data;
[Table("DocumentMeta")]
public class DocumentMetaEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int? ProjectId { get; set; }
[Required]
public string FilePath { get; set; } = string.Empty;
public DateTime? LastModified { get; set; }
public string? Hash { get; set; }
public string? FileType { get; set; }
}

20
Data/ProjectEntity.cs Normal file
View file

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AuthorBuddy.Web.Data;
[Table("Projects")]
public class ProjectEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public string RootPath { get; set; } = string.Empty;
public DateTime? LastOpened { get; set; }
}

14
Data/SettingEntity.cs Normal file
View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AuthorBuddy.Web.Data;
[Table("Settings")]
public class SettingEntity
{
[Key]
public string Key { get; set; } = string.Empty;
[Required]
public string Value { get; set; } = string.Empty;
}

9
Models/Chapter.cs Normal file
View file

@ -0,0 +1,9 @@
namespace AuthorBuddy.Web.Models;
public class Chapter
{
public string Title { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public List<string> Characters { get; set; } = new();
}

14
Models/Constants.cs Normal file
View file

@ -0,0 +1,14 @@
namespace AuthorBuddy.Web.Models;
public static class Constants
{
public static readonly string[] SubFolders = {
"00_Drafts",
"01_Fakten/Personen",
"01_Fakten/Orte",
"02_Archive",
"Exports"
};
public const string SQLiteConnection = "Data Source=author_brain.db";
}

18
Models/ModelStatus.cs Normal file
View file

@ -0,0 +1,18 @@
using Ollama;
namespace AuthorBuddy.Web.Models;
public class ModelStatus
{
public string Status { get; private set; } = "Unknown";
public double Progress { get; private set; }
public void SetModelStatus(PullModelResponse modelResponse)
{
if (modelResponse.Total > 0 && modelResponse.Completed.HasValue && modelResponse.Completed > 0)
{
Progress = (double)modelResponse.Completed.Value / (double)modelResponse.Total * 100;
Status = modelResponse.Status?.ToString() ?? "Unknown";
}
}
}

27
Models/OllamaSettings.cs Normal file
View file

@ -0,0 +1,27 @@
namespace AuthorBuddy.Web.Models;
public class OllamaSettings
{
public string ChatModel { get; set; } = "llama3.2";
public string EmbedModel { get; set; } = "mxbai-embed-large";
public string SpellingModel { get; set; } = "phi3:mini";
public string OllamaUrl { get; set; } = "http://localhost:11434";
public string Theme { get; set; } = "light";
public string LektorSystemPrompt { get; private set; } = "Du bist ein erfahrener Lektor. Analysiere den Text des Autors. " +
"Optimiere ihn auf einen lebendigen, anschaulichen und subjektiven Stil. " +
"Nutze starke Verben, vermeide unnötige Adjektive und achte auf 'Show, don't tell'. " +
"Antworte direkt mit dem verbesserten Text oder konkreten Vorschlägen.";
private string FactCheckTemplate { get; set; } = "FAKTEN AUS DER DATENBANK:\n{0}\n\n" +
"FRAGE / KONTEXT:\n{1}\n\n" +
"Basierend auf den Fakten, schreibe eine kurze, lebendige Ergänzung oder Korrektur.";
public string SpellingPrompt { get; set; } = "Du bist ein Rechtschreibprüfer. Korrigiere NUR Fehler (Rechtschreibung/Grammatik). " +
"Ändere den Stil nicht. Gib nur den korrigierten Text zurück.";
public string GetFactCheckPrompt(string question, string facts)
{
return string.Format(FactCheckTemplate, question, facts);
}
}

8
Models/Project.cs Normal file
View file

@ -0,0 +1,8 @@
namespace AuthorBuddy.Web.Models;
public class Project
{
public int Id { get; internal set; }
public string Name { get; internal set; } = string.Empty;
public string RootPath { get; internal set; } = string.Empty;
}

35
Program.cs Normal file
View file

@ -0,0 +1,35 @@
using AuthorBuddy.Web.Components;
using AuthorBuddy.Web.Data;
using AuthorBuddy.Web.Models;
using AuthorBuddy.Web.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddSingleton<OllamaSettings>();
builder.Services.AddSingleton<IOllamaService, OllamaService>();
builder.Services.AddSingleton<IFileScannerService, FileScannerService>();
builder.Services.AddSingleton<IProjectService, ProjectService>();
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlite(Constants.SQLiteConnection));
var app = builder.Build();
AppDbContext.InitializeDatabase(Constants.SQLiteConnection);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View file

@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5216",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,101 @@
using AuthorBuddy.Web.Data;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace AuthorBuddy.Web.Services;
public class FileScannerService : IFileScannerService
{
private readonly IOllamaService _ollama;
private readonly IDbContextFactory<AppDbContext> _contextFactory;
private readonly string _connectionString;
public FileScannerService(IOllamaService ollama, IDbContextFactory<AppDbContext> contextFactory)
{
_ollama = ollama;
_contextFactory = contextFactory;
_connectionString = Models.Constants.SQLiteConnection;
}
public async Task ScanProjectFolderAsync(int projectId, string rootPath, IProgress<string> progress)
{
var files = Directory.GetFiles(rootPath, "*.md", SearchOption.AllDirectories)
.Where(f => f.Contains("00_Drafts") || f.Contains("01_Fakten"));
foreach (var file in files)
{
var fileInfo = new FileInfo(file);
bool needsUpdate = await CheckIfUpdateRequired(projectId, file, fileInfo.LastWriteTimeUtc);
if (needsUpdate)
{
progress?.Report($"Verarbeite: {fileInfo.Name}...");
await ProcessAndVectorizeFile(projectId, file, fileInfo.LastWriteTimeUtc);
}
}
progress?.Report("Scan abgeschlossen.");
}
private async Task<bool> CheckIfUpdateRequired(int projectId, string file, DateTime lastWriteTimeUtc)
{
await using var context = await _contextFactory.CreateDbContextAsync();
var meta = await context.DocumentMeta
.Where(m => m.ProjectId == projectId && m.FilePath == file)
.Select(m => m.LastModified)
.FirstOrDefaultAsync();
if (meta == null) return true;
return meta.Value < lastWriteTimeUtc;
}
private async Task<bool> ProcessAndVectorizeFile(int projectId, string path, DateTime lastModified)
{
string content = await File.ReadAllTextAsync(path);
var vector = await _ollama.GetEmbeddingAsync(content);
using var connection = new SqliteConnection(_connectionString);
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
var cmdMeta = connection.CreateCommand();
cmdMeta.CommandText = @"
INSERT INTO DocumentMeta (project_id, file_path, last_modified)
VALUES (@pid, @path, @mod)
ON CONFLICT(file_path) DO UPDATE SET last_modified = @mod
RETURNING id;";
cmdMeta.Parameters.AddWithValue("@pid", projectId);
cmdMeta.Parameters.AddWithValue("@path", path);
cmdMeta.Parameters.AddWithValue("@mod", lastModified);
var docId = await cmdMeta.ExecuteScalarAsync();
byte[] blob = ConvertVectorToBytes(vector);
var cmdVec = connection.CreateCommand();
cmdVec.CommandText = "INSERT INTO vec_documents(rowid, embedding) VALUES (@id, @vec) " +
"ON CONFLICT(rowid) DO UPDATE SET embedding = @vec;";
cmdVec.Parameters.AddWithValue("@id", docId);
cmdVec.Parameters.AddWithValue("@vec", blob);
await cmdVec.ExecuteNonQueryAsync();
transaction.Commit();
return true;
}
catch
{
transaction.Rollback();
return false;
}
}
private static byte[] ConvertVectorToBytes(IList<double> vector)
{
float[] floatVector = vector.Select(d => (float)d).ToArray();
byte[] bytes = new byte[floatVector.Length * sizeof(float)];
Buffer.BlockCopy(floatVector, 0, bytes, 0, bytes.Length);
return bytes;
}
}

View file

@ -0,0 +1,6 @@
namespace AuthorBuddy.Web.Services;
public interface IFileScannerService
{
Task ScanProjectFolderAsync(int projectId, string rootPath, IProgress<string> progress);
}

View file

@ -0,0 +1,15 @@
using AuthorBuddy.Web.Models;
using Ollama;
namespace AuthorBuddy.Web.Services;
public interface IOllamaService
{
Task<IEnumerable<Model>> GetLocalModelsAsync();
Task PullModelAsync(string modelName, IProgress<ModelStatus> progress);
Task<IList<double>> GetEmbeddingAsync(string text);
Task<string> StreamStyleCheckAsync(string input);
Task<string> AnswerBasedOnFactsAsync(string question, string facts);
Task<string> QuickSpellCheckAsync(string text);
Task ReconfigureAsync(string ollamaUrl);
}

View file

@ -0,0 +1,10 @@
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public interface IProjectService
{
Task<int> CreateNewProjectAsync(string projectName, string rootPath);
Task<List<Project>> GetAllProjectsAsync();
Task SyncProjectFilesAsync(int projectId, string directoryPath, IProgress<string> progress);
}

84
Services/OllamaService.cs Normal file
View file

@ -0,0 +1,84 @@
using AuthorBuddy.Web.Models;
using Ollama;
namespace AuthorBuddy.Web.Services;
public class OllamaService : IOllamaService, IDisposable
{
private readonly OllamaSettings _settings;
private OllamaApiClient? _client;
private string _currentUrl = "http://localhost:11434";
public OllamaService(OllamaSettings settings)
{
_settings = settings;
_currentUrl = settings.OllamaUrl;
_client = new OllamaApiClient(baseUri: new Uri(_currentUrl));
}
public Task ReconfigureAsync(string ollamaUrl)
{
if (_currentUrl != ollamaUrl)
{
_currentUrl = ollamaUrl;
_client?.Dispose();
_client = new OllamaApiClient(baseUri: new Uri(ollamaUrl));
}
return Task.CompletedTask;
}
private OllamaApiClient Client => _client ??= new OllamaApiClient(baseUri: new Uri(_currentUrl));
public async Task<IEnumerable<Model>> GetLocalModelsAsync()
{
var models = await Client.Models.ListModelsAsync();
return models.Models ?? Enumerable.Empty<Ollama.Model>();
}
public async Task PullModelAsync(string modelName, IProgress<ModelStatus> progress)
{
var p = new ModelStatus();
await foreach (var response in Client.Models.PullModelAsync(modelName))
{
p.SetModelStatus(response);
progress?.Report(p);
}
}
public async Task<IList<double>> GetEmbeddingAsync(string text)
{
var response = await Client.Embeddings.GenerateEmbeddingAsync(
model: _settings.EmbedModel,
prompt: text);
return response.Embedding ?? Array.Empty<double>();
}
public async Task<string> StreamStyleCheckAsync(string input)
{
var systemPrompt = _settings.LektorSystemPrompt;
var chat = Client.Chat(_settings.ChatModel, systemPrompt);
var message = await chat.SendAsync(input);
return message.Content;
}
public async Task<string> AnswerBasedOnFactsAsync(string question, string facts)
{
var prompt = _settings.GetFactCheckPrompt(question, facts);
var chat = Client.Chat(_settings.ChatModel);
var message = await chat.SendAsync(prompt);
return message.Content;
}
public async Task<string> QuickSpellCheckAsync(string text)
{
var systemPrompt = _settings.SpellingPrompt;
var chat = Client.Chat(_settings.SpellingModel, systemPrompt);
var message = await chat.SendAsync(text);
return message.Content;
}
public void Dispose()
{
_client?.Dispose();
}
}

View file

@ -0,0 +1,63 @@
using AuthorBuddy.Web.Data;
using AuthorBuddy.Web.Models;
using Microsoft.EntityFrameworkCore;
namespace AuthorBuddy.Web.Services;
public class ProjectService : IProjectService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
private readonly IFileScannerService _fileScannerService;
public ProjectService(IDbContextFactory<AppDbContext> contextFactory, IFileScannerService fileScannerService)
{
_contextFactory = contextFactory;
_fileScannerService = fileScannerService;
}
public async Task<int> CreateNewProjectAsync(string projectName, string rootPath)
{
string[] subFolders = { "00_Drafts", "01_Fakten", "02_Characters", "03_Archive" };
foreach (var subFolder in subFolders)
{
Directory.CreateDirectory(Path.Combine(rootPath, subFolder));
}
string readmePath = Path.Combine(rootPath, "00_Projekt_Info.md");
await File.WriteAllTextAsync(readmePath, $"# Projekt: {projectName}\nErstellt am: {DateTime.Now}");
await using var context = await _contextFactory.CreateDbContextAsync();
var entity = new ProjectEntity
{
Name = projectName,
RootPath = rootPath,
LastOpened = DateTime.Now
};
context.Projects.Add(entity);
await context.SaveChangesAsync();
return entity.Id;
}
public async Task<List<Project>> GetAllProjectsAsync()
{
await using var context = await _contextFactory.CreateDbContextAsync();
return await context.Projects
.OrderByDescending(p => p.LastOpened)
.Select(p => new Project
{
Id = p.Id,
Name = p.Name,
RootPath = p.RootPath
})
.ToListAsync();
}
public async Task SyncProjectFilesAsync(int projectId, string directoryPath, IProgress<string> progress)
{
await _fileScannerService.ScanProjectFolderAsync(projectId, directoryPath, progress);
}
}

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

BIN
author_brain.db Normal file

Binary file not shown.

BIN
vec0.dll Normal file

Binary file not shown.

367
wwwroot/app.css Normal file
View file

@ -0,0 +1,367 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--window-bg);
color: var(--text-color);
height: 100vh;
overflow: hidden;
}
/* Navigation panel (sidebar accordion) */
.nav-section {
margin-bottom: 2px;
}
.nav-section-header {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 6px 8px;
background: var(--menu-bg);
border: 1px solid var(--border-color);
cursor: pointer;
font-size: 13px;
font-weight: bold;
text-align: left;
user-select: none;
}
.nav-section-header:hover {
background: var(--accent-color);
color: white;
}
.nav-icon {
font-size: 10px;
width: 12px;
}
.nav-section-body {
border: 1px solid var(--border-color);
border-top: none;
background: var(--card-bg);
}
.nav-item {
display: block;
width: 100%;
padding: 6px 16px;
background: none;
border: none;
cursor: pointer;
font-size: 13px;
text-align: left;
color: var(--text-color);
}
.nav-item:hover {
background: var(--accent-color);
color: white;
}
.nav-separator {
height: 1px;
background: var(--border-color);
margin: 4px 8px;
}
.nav-hint {
display: block;
padding: 6px 16px;
font-size: 12px;
color: var(--border-color);
}
/* Status bar */
.status-bar {
background: var(--status-bar-bg);
border-top: 2px solid var(--border-color);
display: flex;
align-items: center;
padding: 3px 8px;
font-size: 12px;
min-height: 28px;
gap: 8px;
}
.status-bar .status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.status-bar .status-dot.connected {
background: #00cc00;
}
.status-bar .status-dot.disconnected {
background: #cc0000;
}
.status-bar .status-text {
flex: 1;
}
.status-bar progress {
width: 80px;
height: 12px;
}
/* Main content area */
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 250px;
min-width: 150px;
border-right: 2px solid var(--border-color);
padding: 4px;
overflow-y: auto;
background: var(--window-bg);
}
/* Detail panel */
.detail-panel {
flex: 1;
padding: 12px;
overflow-y: auto;
}
.detail-panel .tab-bar {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 12px;
}
.detail-panel .tab-bar .tab {
padding: 8px 16px;
cursor: pointer;
border: 1px solid transparent;
margin-bottom: -2px;
}
.detail-panel .tab-bar .tab.active {
border: 1px solid var(--border-color);
border-bottom-color: var(--window-bg);
background: var(--card-bg);
}
.detail-panel .tab-content {
padding: 8px;
}
/* Cards for RAG facts */
.fact-card {
background: var(--card-bg);
border-left: 3px solid var(--accent-color);
padding: 12px;
margin-bottom: 8px;
border-radius: 2px;
font-size: 14px;
line-height: 1.5;
}
/* Editor */
.editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
.editor-container textarea {
flex: 1;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14pt;
padding: 20px;
border: none;
outline: none;
resize: none;
background: var(--editor-bg);
color: var(--text-color);
line-height: 1.6;
}
.editor-container .editor-toolbar {
display: flex;
justify-content: flex-end;
padding: 8px 16px;
gap: 8px;
border-top: 1px solid var(--border-color);
background: var(--menu-bg);
}
/* Buttons */
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid var(--border-color);
background: var(--menu-bg);
color: var(--text-color);
font-size: 13px;
}
button:hover {
background: var(--accent-color);
color: white;
}
button.primary {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
/* Forms */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 4px;
font-size: 13px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 6px 8px;
border: 1px solid var(--border-color);
background: var(--editor-bg);
color: var(--text-color);
font-size: 13px;
}
.form-actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
/* Project tree */
.tree-node {
padding: 4px 8px;
cursor: pointer;
font-size: 13px;
}
.tree-node:hover {
background: var(--accent-color);
color: white;
}
.tree-node .children {
padding-left: 20px;
}
/* Grid splitter simulation */
.grid-splitter {
width: 5px;
cursor: col-resize;
background: transparent;
}
/* Progress */
.progress-bar {
width: 100%;
height: 20px;
background: var(--border-color);
border-radius: 2px;
overflow: hidden;
}
.progress-bar .progress-fill {
height: 100%;
background: var(--accent-color);
transition: width 0.3s;
}
/* Model list */
.model-list {
display: grid;
gap: 8px;
}
.model-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
padding: 12px;
border-radius: 4px;
}
.model-card .model-name {
font-weight: bold;
font-size: 14px;
}
.model-card .model-details {
font-size: 12px;
color: var(--border-color);
margin-top: 4px;
}
/* Settings page */
.settings-page {
max-width: 600px;
}
.settings-section {
background: var(--card-bg);
border: 1px solid var(--border-color);
padding: 16px;
margin-bottom: 12px;
}
.settings-section-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 12px;
color: var(--accent-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
}
.form-hint {
font-size: 11px;
color: var(--border-color);
margin-top: 4px;
}
.settings-status {
margin-top: 16px;
padding: 10px;
background: var(--card-bg);
border: 1px solid var(--accent-color);
font-size: 13px;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--window-bg);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}

View file

@ -0,0 +1,10 @@
:root {
--window-bg: #1e1e1e;
--editor-bg: #2d2d2d;
--text-color: #d4d4d4;
--border-color: #3c3c3c;
--accent-color: #569cd6;
--menu-bg: #252526;
--card-bg: #2d2d2d;
--status-bar-bg: #007acc;
}

View file

@ -0,0 +1,10 @@
:root {
--window-bg: #f0f0f0;
--editor-bg: #ffffff;
--text-color: #1a1a1a;
--border-color: #cccccc;
--accent-color: #2b5797;
--menu-bg: #e0e0e0;
--card-bg: #ffffff;
--status-bar-bg: #dcdcdc;
}

View file

@ -0,0 +1,10 @@
:root {
--window-bg: #c0c0c0;
--editor-bg: #ffffff;
--text-color: #000000;
--border-color: #808080;
--accent-color: #000080;
--menu-bg: #c0c0c0;
--card-bg: #ffffff;
--status-bar-bg: #c0c0c0;
}

BIN
wwwroot/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more